MCP Swift SDK

repository·main·Indexed 23 days ago

https://github.com/modelcontextprotocol/swift-sdk

The official Swift implementation of the Model Context Protocol (MCP), providing client and server components for standardized communication between applications and AI/ML models. It supports Stdio and HTTP transports, tool execution, resource subscription, prompt management, and advanced features like sampling, elicitation, and request batching. Requires Swift 6.0+ and Xcode 16+.

Tokens
12.9K
Snippets
29
Records
52
Agent score
81%

What's inside MCP Swift SDK

  1. Implement Sampling and Elicitation Handlers

    main

    Sampling and Elicitation are flows where the server requests information from the client.

    • Sampling: The server requests an LLM completion. Use withSamplingHandler to intercept the request, potentially review it (human-in-the-loop), call your own LLM service, and return a CreateSamplingMessage.Result.
    • Elicitation: The server requests structured information (like credentials or user confirmation) via a form or a URL. Use withElicitationHandler to present a UI to the user and return a CreateElicitation.Result (accept, cancel, or decline).
    // Sampling Handler
    await client.withSamplingHandler { parameters in
        let completion = try await callYourLLMService(messages: parameters.messages)
        return CreateSamplingMessage.Result(
            model: "my-model",
            stopReason: .endTurn,
            role: .assistant,
            content: .text(completion)
        )
    }
    
    // Elicitation Handler
    await client.withElicitationHandler { parameters in
        switch parameters {
        case .form(let form):
            let userResponse = presentElicitationUI(form)
            return CreateElicitation.Result(action: .accept, content: userResponse.data)
        case .url(let url):
            openURL(url.url)
            return CreateElicitation.Result(action: .accept)
        }
    }
  2. Enable Debugging and Logging

    main

    Use the Logging framework to configure a global LoggingSystem and pass a Logger instance to your clients, servers, or transports.

    import Logging
    import MCP
    
    // Configure Logger
    LoggingSystem.bootstrap { label in
        var handler = StreamLogHandler.standardOutput(label: label)
        handler.logLevel = .debug
        return handler
    }
    
    // Create logger
    let logger = Logger(label: "com.example.mcp")
    
    // Pass to client/server
    let client = Client(name: "MyApp", version: "1.0.0")
    
    // Pass to transport
    let transport = StdioTransport(logger: logger)
  3. Configure Client Transports (Stdio and HTTP)

    main

    The SDK provides different transport options depending on whether the server is a local subprocess or a remote service.

    • StdioTransport: Used for local subprocess communication. This is the simplest option.
    • HTTPClientTransport: Used for remote server communication. Setting streaming: true enables Server-Sent Events (SSE) for real-time updates.
    // Stdio Transport (Local)
    let transport = StdioTransport()
    try await client.connect(transport: transport)
    
    // HTTP Transport (Remote)
    let transport = HTTPClientTransport(
        endpoint: URL(string: "http://localhost:8080")!,
        streaming: true
    )
    try await client.connect(transport: transport)
  4. Implement Tool Handlers

    main

    Tools allow clients to execute specific functions. You must register two types of handlers:

    1. ListTools: Returns an array of Tool objects defining the tool's name, description, and inputSchema (using JSON Schema-like structure).
    2. CallTool: Executes the logic when a tool is called. It receives params containing the tool name and arguments. You should return a result containing content (e.g., .text) and an isError flag.
    // Register a tool list handler
    await server.withMethodHandler(ListTools.self) { _ in
        let tools = [
            Tool(
                name: "weather",
                description: "Get current weather for a location",
                inputSchema: .object([
                    "properties": .object([
                        "location": .string("City name or coordinates"),
                        "units": .string("Units of measurement, e.g., metric, imperial")
                    ])
                ])
            )
        ]
        return .init(tools: tools)
    }
    
    // Register a tool call handler
    await server.withMethodHandler(CallTool.self) { params in
        switch params.name {
        case "weather":
            let location = params.arguments?["location"]?.stringValue ?? "Unknown"
            // ... implementation ...
            return .init(
                content: [.text("Weather for \(location): ...")],
                isError: false
            )
        default:
            return .init(content: [.text("Unknown tool")], isError: true)
        }
    }
  5. Send Structured Logs to Clients

    main

    Servers can send structured log messages to clients using server.log(level:logger:data:). The data parameter can be a Value object or any Codable type. Clients can control the verbosity by sending a SetLoggingLevel request to the server.

    // Send a log message with a Codable struct
    struct ErrorLog: Codable {
        let message: String
        let code: Int
    }
    
    let errorLog = ErrorLog(message: "Failed", code: 500)
    try await server.log(level: .error, logger: "operations", data: errorLog)
  6. Manage Server Lifecycle with Swift Service Lifecycle

    main

    For production-grade servers, it is recommended to use the Swift Service Lifecycle library to manage startup, shutdown, and signal handling (SIGINT/SIGTERM). Implement the Service protocol to wrap your Server and Transport, ensuring server.stop() is called during a graceful shutdown.

    import MCP
    import ServiceLifecycle
    
    struct MCPService: Service {
        let server: Server
        let transport: Transport
    
        func run() async throws {
            try await server.start(transport: transport)
            try await Task.sleep(for: .days(365 * 100))
        }
    
        func shutdown() async throws {
            await server.stop()
        }
    }
    
    // Use ServiceGroup to manage multiple services and signals
    let serviceGroup = ServiceGroup(
        services: [mcpService, databaseService],
        configuration: .init(gracefulShutdownSignals: [.sigterm, .sigint]),
        logger: logger
    )
    try await serviceGroup.run()
  7. Configure OAuth 2.1 Authentication for HTTPClientTransport

    main
    Authentication is opt-in. To enable it, pass an OAuthAuthorizer to HTTPClientTransport(authorizer:). The SDK handles automatic discovery of metadata and token acquisition (including PKCE enforcement) when a 401 or 403 is received.
  8. Request User Information via Elicitation

    main
    Servers can request specific information from the user using requestElicitation. You define a Elicitation.RequestSchema describing the required fields. The server waits for the user to respond. The result can be .accept (with the collected content), .decline, or .cancel. For OAuth or similar flows, use the URL-based overload.
  9. Request LLM completions via Sampling

    main

    Servers can request LLM completions from the connected client using requestSampling. This allows the server to perform agentic tasks like decision-making or content generation by asking the client's AI for assistance. This requires the sampling capability to be enabled in the server's capabilities object.

    // Enable sampling capability
    let server = Server(
        name: "MyModelServer",
        version: "1.0.0",
        capabilities: .init(sampling: .init())
    )
    
    // Request sampling
    do {
        let result = try await server.requestSampling(
            messages: [.user("Analyze this data")],
            systemPrompt: "You are a helpful analyst",
            temperature: 0.7,
            maxTokens: 150
        )
        print("LLM suggested: \(result.content)")
    } catch {
        print("Sampling failed: \(error)")
    }
  10. Implement Completion Handlers

    main

    Servers can provide autocompletion for prompt and resource template arguments using the Complete handler. The handler receives params containing the argument being completed and a ref (either .prompt or .resource). You can also access params.context to provide suggestions based on previously resolved arguments in the same session.

    // Register a completion handler
    await server.withMethodHandler(Complete.self) { params in
        switch params.ref {
        case .prompt(let promptRef):
            if promptRef.name == "code_review" && params.argument.name == "language" {
                let matches = ["python", "swift"].filter { $0.hasPrefix(params.argument.value.lowercased()) }
                return .init(completion: .init(values: matches, total: matches.count, hasMore: false))
            }
        case .resource(let resourceRef):
            // Handle resource template completions
            break
        }
        return .init(completion: .init(values: [], total: 0, hasMore: false))
    }
  11. Implement Prompt Handlers

    main

    Prompts are reusable conversation starters. Implement:

    • ListPrompts: Returns a list of Prompt objects, including their name, description, and required/optional arguments.
    • GetPrompt: Returns the specific Prompt details, including a description and a sequence of messages (user or assistant) to seed the conversation.
    // Register a prompt list handler
    await server.withMethodHandler(ListPrompts.self) { params in
        let prompts = [
            Prompt(
                name: "interview",
                description: "Job interview conversation starter",
                arguments: [
                    .init(name: "position", description: "Job position", required: true)
                ]
            )
        ]
        return .init(prompts: prompts, nextCursor: nil)
    }
    
    // Register a prompt get handler
    await server.withMethodHandler(GetPrompt.self) { params in
        switch params.name {
        case "interview":
            let messages: [Prompt.Message] = [
                .user(.text(text: "You are an interviewer...")),
                .assistant(.text(text: "Hi, welcome!"))
            ]
            return .init(description: "Interview prompt", messages: messages)
        default:
            throw MCPError.invalidParams("Unknown prompt name: \(params.name)")
        }
    }