Skip to content

feat: add EchoServer example and update docs #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Example/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions Example/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "dummy-mcp-server",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.7.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "dummy-mcp-server",
dependencies: [
.product(name: "MCP", package: "swift-sdk")
]
),
]
)
45 changes: 45 additions & 0 deletions Example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Basic Stdio Example

This is a basic example demonstrating how to create a simple server that communicates over standard input/output (stdio) using the Model Context Protocol Swift SDK. This setup is compatible with applications like Claude Desktop.

## Getting Started

https://github.com/user-attachments/assets/8e790235-a34c-4358-981d-4d4b5e91488f

Located in the `Example` directory, this simple server demonstrates:
* Setting up a basic MCP server.
* Defining and registering a custom tool (`swift_echo`).
* Handling `ListTools` and `CallTool` requests.
* Using `StdioTransport` for communication.
* Detailed logging to stderr.

**Running the Echo Server:**

1. Navigate to the example directory:
```bash
cd Example
```
2. Build the server:
```bash
swift build -c release
```
This will create the executable at `.build/release/EchoServer`. Note the full path to this executable.

3. **Configure Claude Desktop:**
To make this server available as a tool provider in Claude Desktop, you need to add it to your Claude Desktop configuration file (`claude_desktop_config.json`). The location of this file varies by operating system.

Add an entry like the following to the `mcpServers` object in your `claude_desktop_config.json`, replacing `/PATH/TO/YOUR/SERVER/` with the actual absolute path to the `swift-sdk` directory on your system:

```json
{
"mcpServers": {
// ... other servers maybe ...
"swift_echo_example": {
"command": "/PATH/TO/YOUR/SERVER/Example/.build/release/dummy-mcp-server"
}
// ... other servers maybe ...
}
}
```
* `swift_echo_example`: This is the name you'll refer to the server by within Claude Desktop (you can change this).
* `command`: This **must** be the absolute path to the compiled `EchoServer` executable you built in the previous step.https://github.com/modelcontextprotocol/swift-sdk/pull/55#issuecomment-2781042026
131 changes: 131 additions & 0 deletions Example/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import MCP
import Foundation

// Async helper function to set up and start the server
// Returns the Server instance
// (Keep detailed logs)
func setupAndStartServer() async throws -> Server {
fputs("log: setupAndStartServer: entering function.\n", stderr)

// Define a JSON schema for the tool's input
let echoInputSchema: Value = .object([
"type": .string("object"),
"properties": .object([
"message": .object([
"type": .string("string"),
"description": .string("The text to echo back")
])
]),
"required": .array([.string("message")])
])
fputs("log: setupAndStartServer: defined input schema for echo tool.\n", stderr)

// Define the echo tool using the schema
let echoTool = Tool(
name: "swift_echo",
description: "A simple tool that echoes back its input arguments.",
inputSchema: echoInputSchema
)
fputs("log: setupAndStartServer: defined echo tool: \(echoTool.name) with detailed inputSchema.\n", stderr)

let server = Server(
name: "SwiftEchoServer",
version: "1.0.0",
capabilities: .init(
tools: .init(listChanged: false)
)
)
fputs("log: setupAndStartServer: server instance created (\(server.name)) with capabilities: tools(listChanged: false).\n", stderr)

// Register handlers
await server.withMethodHandler(ReadResource.self) { params in
let uri = params.uri
fputs("log: handler(ReadResource): received request for uri: \(uri)\n", stderr)
let content = [Resource.Content.text("dummy content for \(uri)", uri: uri)]
return .init(contents: content)
}
fputs("log: setupAndStartServer: registered ReadResource handler.\n", stderr)

await server.withMethodHandler(ListResources.self) { _ in
fputs("log: handler(ListResources): received request (dummy handler - unlikely to be called).\n", stderr)
return ListResources.Result(resources: [])
}
fputs("log: setupAndStartServer: registered ListResources handler (dummy).\n", stderr)

await server.withMethodHandler(ListPrompts.self) { _ in
fputs("log: handler(ListPrompts): received request (dummy handler - unlikely to be called).\n", stderr)
return ListPrompts.Result(prompts: [])
}
fputs("log: setupAndStartServer: registered ListPrompts handler (dummy).\n", stderr)

await server.withMethodHandler(ListTools.self) { _ in
fputs("log: handler(ListTools): received request.\n", stderr)
let result = ListTools.Result(tools: [echoTool])
fputs("log: handler(ListTools): responding with: \(result.tools.map { $0.name }) (detailed schema)\n", stderr)
return result
}
fputs("log: setupAndStartServer: registered ListTools handler.\n", stderr)

await server.withMethodHandler(CallTool.self) { params in
fputs("log: handler(CallTool): received request for tool: \(params.name).\n", stderr)
if params.name == echoTool.name {
let messageToEcho: String
if let args = params.arguments, let messageValue = args["message"], let msgStr = messageValue.stringValue {
messageToEcho = msgStr
} else {
fputs("log: handler(CallTool): warning: 'message' argument not found or not a string in args: \(params.arguments?.description ?? "nil")\n", stderr)
messageToEcho = params.arguments?.description ?? "no arguments provided"
}

let content = [Tool.Content.text(messageToEcho)]
fputs("log: handler(CallTool): executing echo tool with message: \(messageToEcho)\n", stderr)
fputs("log: handler(CallTool): responding with echoed content.\n", stderr)
return .init(content: content, isError: false)
} else {
fputs("log: handler(CallTool): error: tool not found: \(params.name)\n", stderr)
throw MCPError.methodNotFound(params.name)
}
}
fputs("log: setupAndStartServer: registered CallTool handler.\n", stderr)

let transport = StdioTransport()
fputs("log: setupAndStartServer: created StdioTransport.\n", stderr)

// Start the server (assuming it runs in background and returns)
fputs("log: setupAndStartServer: calling server.start()...\n", stderr)
try await server.start(transport: transport)
fputs("log: setupAndStartServer: server.start() completed (background task launched).\n", stderr)

// Return the server instance
fputs("log: setupAndStartServer: returning server instance.\n", stderr)
return server
}

@main
struct MCPServer {
// Main entry point - Async
static func main() async {
fputs("log: main: starting (async).\n", stderr)

let server: Server
do {
fputs("log: main: calling setupAndStartServer()...\n", stderr)
server = try await setupAndStartServer()
fputs("log: main: setupAndStartServer() successful, server instance obtained.\n", stderr)

// Explicitly wait for the server's internal task to complete.
// This will block the main async task until the server stops listening.
fputs("log: main: server started, calling server.waitUntilCompleted()...\n", stderr)
await server.waitUntilCompleted()
fputs("log: main: server.waitUntilCompleted() returned. Server has stopped.\n", stderr)

} catch {
fputs("error: main: server setup/run failed: \(error)\n", stderr)
exit(1)
}

// Server has stopped either gracefully or due to error handled by waitUntilCompleted.
fputs("log: main: Server processing finished. Exiting.\n", stderr)
// No need to call server.stop() here as waitUntilCompleted only returns when the server *is* stopped.
}
}
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,54 @@ let (description, messages) = try await client.getPrompt(
)
```

## Examples

This repository includes an example server demonstrating how to use the Swift MCP SDK.

### Echo Server

Located in the `Example` directory, this simple server demonstrates:
* Setting up a basic MCP server.
* Defining and registering a custom tool (`swift_echo`).
* Handling `ListTools` and `CallTool` requests.
* Using `StdioTransport` for communication.
* Detailed logging to stderr.

**Running the Echo Server:**

1. Navigate to the example directory:
```bash
cd Example
```
2. Build the server:
```bash
swift build -c release
```
This will create the executable at `.build/release/EchoServer`. Note the full path to this executable.

3. **Configure Claude Desktop:**
To make this server available as a tool provider in Claude Desktop, you need to add it to your Claude Desktop configuration file (`claude_desktop_config.json`). The location of this file varies by operating system.

Add an entry like the following to the `mcpServers` object in your `claude_desktop_config.json`, replacing `/PATH/TO/YOUR/SERVER/` with the actual absolute path to the `swift-sdk` directory on your system:

```json
{
"mcpServers": {
// ... other servers maybe ...
"swift_echo_example": {
"command": "/PATH/TO/YOUR/SERVER/Example/.build/release/dummy-mcp-server"
}
// ... other servers maybe ...
}
}
```
* `swift_echo_example`: This is the name you'll refer to the server by within Claude Desktop (you can change this).
* `command`: This **must** be the absolute path to the compiled `EchoServer` executable you built in the previous step.

4. **Restart Claude Desktop:** After saving the changes to `claude_desktop_config.json`, restart Claude Desktop for the new server configuration to be loaded. The `swift_echo` tool should then be available.

The server will be started automatically by Claude Desktop when needed and will communicate over standard input/output, printing detailed logs to standard error (which might be captured by Claude Desktop's logs).

## Changelog

This project follows [Semantic Versioning](https://semver.org/).
Expand Down