SwiftMCP: Model Context Protocol for Swift

repository·main·Indexed 41 days ago

https://github.com/cocoanetics/swiftmcp

A Swift implementation of the Model Context Protocol (MCP) for building servers and clients using JSON-RPC. It supports multiple transports including stdio, HTTP+SSE, and TCP+Bonjour. Features include @MCPServer, @MCPTool, and @MCPResource macros for rapid server development, typed client proxies, and integrated JWT decoding and OAuth validation for secure communication.

Tokens
13.1K
Snippets
42
Records
52
Agent score
51%

What's inside SwiftMCP

  1. Key features of SwiftMCP

    main

    SwiftMCP provides several automated capabilities for building AI-ready tools:

    • Documentation-Driven: Automatically converts standard Swift documentation comments into tool descriptions, parameter information, and return type documentation.
    • Type-Safe: Automatically validates and converts incoming JSON-RPC parameters into the correct Swift types.
    • AI-Ready: Includes built-in support for generating OpenAPI specifications and AI plugin manifests.
    • Flexible Transport: Supports multiple communication methods, including HTTP+SSE (for web applications) and stdio (for command-line tools).
  2. Discover services via TCP+Bonjour

    main

    The TCPBonjourTransport allows for local discovery of MCP services. Identity is managed via the instance name and a scope.

    Scopes:

    • .localUser (Default): Advertises as Name (user). Limits discovery to the same user on the same machine to prevent collisions between different user accounts.
    • .localMachine: Advertises as Name. Visible on the local loopback.
    • .localNetwork: Advertises on the attached link. Note that TCP transport has no built-in authorization, so anything on the network can reach the service.

    To connect to a discovered service, the client must use the same base name and scope as the server.

    // Server side
    let transport = TCPBonjourTransport(server: server) // Defaults to .localUser
    
    // Client side
    let config = MCPServerTcpConfig(instanceName: "MyServerName") // Matches .localUser
    let proxy = MCPServerProxy(config: .tcp(config: config))
    try await proxy.connect()
  3. How decoupled transports work in SwiftMCP

    main

    SwiftMCP separates the responsibility of moving bytes (the transport) from the responsibility of answering messages (the server). This is achieved through two protocols:

    1. MCPDispatcher: The dispatch surface that a transport calls. It provides methods to handle a single JSONRPCMessage or a batch of messages. The dispatcher manages the 'gate' (e.g., rejecting requests before initialization).
    2. MCPTransport: A Service that owns the wire and its sessions. It routes decoded payloads through an injected MCPDispatcher.

    The MCPServer.serve(over:...) method acts as the glue, connecting the dispatcher to every transport and managing their lifecycle and ordered shutdown.

    public protocol MCPDispatcher: Sendable {
        func handle(_ message: JSONRPCMessage) async -> JSONRPCMessage?
        func handle(_ batch: [JSONRPCMessage]) async -> [JSONRPCMessage]
    }
    
    public protocol MCPTransport: Service {
        func connect(to dispatcher: any MCPDispatcher)
    }
  4. Provide dynamic resources with MCPResourceProviding

    main

    To provide a dynamic list of resources that can change at runtime, conform your server to the MCPResourceProviding protocol. You must implement the mcpResources property, which returns an array of any MCPResource.

    extension DemoServer: MCPResourceProviding {
        var mcpResources: [any MCPResource] {
            get async { await getDynamicFileResources() }
        }
    }
  5. Provide parameter completions via MCPCompletionProviding

    main

    To provide specific suggestion values for tool parameters, conform your server to the MCPCompletionProviding protocol.

    If you do not provide custom completions, SwiftMCP provides default behavior:

    • Bool parameters: Provides default suggestions.
    • CaseIterable enums: Automatically provides all cases of the enum as completion suggestions.
  6. Work with Client Roots

    main

    Roots are filesystem locations provided by the client. You can retrieve the list of available roots using session.listRoots().

    To react to changes in the client's filesystem environment, implement the handleRootsListChanged() method within your @MCPServer actor.

    @MCPTool
    func listClientRoots() async throws -> [String] {
        guard let session = Session.current else {
            throw MCPServerError.noActiveSession
        }
        
        guard await session.clientCapabilities?.roots != nil else {
            return ["Client does not support roots"]
        }
        
        let roots = try await session.listRoots()
        return roots.map { "\(\($0.name ?? "Unnamed")): \($0.uri)" }
    }
    
    @MCPServer(name: "MyServer")
    actor MyServer {
        func handleRootsListChanged() async {
            guard let session = Session.current else { return }
            do {
                let updatedRoots = try await session.listRoots()
                // Handle updated roots...
            } catch {
                // Handle error...
            }
        }
    }
  7. Security Warning: Signature Verification

    main

    ⚠️ CRITICAL: The JWTDecoder validates JWT structure and claims (like iss, aud, exp) but does NOT verify cryptographic signatures.

    For production environments, you must manually:

    1. Fetch the public key from your provider's JWKS endpoint (e.g., https://your-domain.auth0.com/.well-known/jwks.json).
    2. Verify the signature using the public key that matches the kid (Key ID) found in the token header.
    3. Implement key caching and rotation.

    This implementation is intended for development, testing, or scenarios where tokens are already validated by a trusted proxy.

  8. How prompt completions work

    main

    Prompts support completion (providing suggestions for parameters) in the same way tools and resources do.

    • Automatic Completions: Parameters of type Bool or enums conforming to CaseIterable automatically receive sensible completion suggestions.
    • Custom Completions: For more complex parameter types, implement the MCPCompletionProviding protocol to provide custom suggestions.
  9. Expose read-only data with @MCPResource

    main

    The @MCPResource macro allows you to expose functions as resources accessible via URI templates. This enables clients to access data using structured URIs with path and query parameters.

    Key Features

    • URI Templates: Define patterns using curly braces for placeholders (e.g., {param}).
    • Path Parameters: Extract values directly from the URI path.
    • Query Parameters: Extract values from the query string (e.g., ?q={query}).
    • Optional Parameters: Supports Swift default values for optional parameters.
    • MIME Types: You can specify the content type using the mimeType: parameter.
    • Type Safety: Parameters are automatically converted from URI strings to the correct Swift types.

    Automatic Server Capabilities

    When using @MCPResource, the server automatically handles:

    • Resource discovery via mcpResourceTemplates.
    • URI matching and parameter extraction.
    • Type conversion, validation, and error handling for invalid parameters.
    @MCPServer(name: "ResourceServer")
    actor ResourceServer {
        /// Get user profile by ID
        @MCPResource("users://{user_id}/profile")
        func getUserProfile(user_id: Int) -> String {
            return "Profile for user \(user_id)"
        }
        
        /// Search users with pagination
        @MCPResource("users://search?q={query}&page={page}", mimeType: "application/json")
        func searchUsers(query: String, page: Int = 1) -> String {
            return "{\"query\": الش\"query\"\", \"page\": \(page)\"}"
        }
    }
  10. Choose the right server type (struct vs actor)

    main

    Because transports no longer hold a reference to the server, the server's type should be chosen based on its domain state:

    • Use a struct if the server has no domain state. It is trivially Sendable and safe for concurrent calls.
    • Use an actor (or a thread-safe class) if the server requires shared mutable state.
    @MCPServer(name: "calc")
    struct Calculator {
        @MCPTool func add(a: Int, b: Int) -> Int { a + b }
    }