diff --git a/Example/Package.resolved b/Example/Package.resolved new file mode 100644 index 0000000..867d0a3 --- /dev/null +++ b/Example/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", + "version" : "1.6.3" + } + }, + { + "identity" : "swift-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/modelcontextprotocol/swift-sdk.git", + "state" : { + "revision" : "402904eab2edadfc4ff1fe143ad6e33b87b22de2", + "version" : "0.7.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", + "version" : "1.4.2" + } + } + ], + "version" : 2 +} diff --git a/Example/Package.swift b/Example/Package.swift new file mode 100644 index 0000000..be95349 --- /dev/null +++ b/Example/Package.swift @@ -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") + ] + ), + ] +) diff --git a/Example/README.md b/Example/README.md new file mode 100644 index 0000000..2b9ec06 --- /dev/null +++ b/Example/README.md @@ -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 diff --git a/Example/Sources/main.swift b/Example/Sources/main.swift new file mode 100644 index 0000000..28a89c6 --- /dev/null +++ b/Example/Sources/main.swift @@ -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. + } +} diff --git a/README.md b/README.md index 01a5331..b0fd835 100644 --- a/README.md +++ b/README.md @@ -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/).