Skip to content

ajevans99/swift-mcp-toolkit

Repository files navigation

swift-mcp-toolkit

CI Image Image

A toolkit built on top of the official Swift SDK for Model Context Protocol server and clients that makes it easy to define strongly-typed tools.

Quick Start

Step 1: Define a Tool

Conform to MCPTool, describe your parameters using the JSONSchemaBuilder or @Schemable from swift-json-schema, and implement the call(with:) method.

struct WeatherTool: MCPTool {
  let name = "weather"
  let description: String? = "Return the weather for a location"

  @Schemable
  enum Unit {
    case fahrenheit
    case celsius
  }

  @Schemable
  @ObjectOptions(.additionalProperties { false })
  struct Parameters {
    /// Location as city, like "Detroit" or "New York"
    let location: String

    /// Unit for temperature
    let unit: Unit
  }

  func call(with arguments: Parameters) async throws(ToolError) -> Content {
    switch arguments.unit {
    case .fahrenheit:
      "The weather in \(arguments.location) is 75°F and sunny."
    case .celsius:
      "The weather in \(arguments.location) is 24°C and sunny."
    }
  }
}
Compare to see the vanilla swift-sdk approach
// Example/Sources/MCPToolkitExample/Tools/VanillaWeatherTool.swift
import MCP

struct VanillaWeatherTool {
  static let name = "weather"

  static func configure(server: Server) async {
    await server.withMethodHandler(ListTools.self) { _ in
      let tools = [
        Tool(
          name: Self.name,
          description: "Return the weather for a location",
          inputSchema: .object([
            "type": .string("object"),
            "additionalProperties": .bool(false),
            "properties": .object([
              "location": .object([
                "type": .string("string"),
                "description": .string("Location as city, like \"Detroit\" or \"New York\""),
              ]),
              "unit": .object([
                "type": .string("string"),
                "enum": .array(["fahrenheit", "celsius"].map { .string($0) }),
                "description": .string("Unit for temperature"),
              ]),
            ]),
            "required": .array([.string("location"), .string("unit")]),
          ])
        )
      ]
      return .init(tools: tools)
    }

    await server.withMethodHandler(CallTool.self) { params async in
      guard let arguments = params.arguments else {
        return .init(
          content: [.text("Missing arguments for tool \(Self.name)")],
          isError: true
        )
      }

      guard
        case .string(let location)? = arguments["location"],
        case .string(let unit)? = arguments["unit"]
      else {
        return .init(
          content: [.text("Arguments for tool \(Self.name) failed validation.")],
          isError: true
        )
      }

      let summary: String
      switch unit {
      case "fahrenheit":
        summary = "The weather in \(location) is 75°F and sunny."
      case "celsius":
        summary = "The weather in \(location) is 24°C and sunny."
      default:
        return .init(
          content: [.text("Arguments for tool \(Self.name) failed validation.")],
          isError: true
        )
      }

      return .init(content: [.text(summary)])
    }
  }
}

Step 2: Register the Tool with a MCP Server

Create the same Server instance you would when using the swift-sdk, then call register(tools:) with your tool instance(s). The optional messaging: parameter lets you customise every toolkit-managed response if you want to adjust tone, add metadata, or localise error messages.

import MCPToolkit

let server = Server(
  name: "Weather Station",
  version: "1.0.0",
  capabilities: .init(tools: .init(listChanged: true))
)

await server.register(
  tools: [WeatherTool()],
  messaging: ResponseMessagingFactory.defaultWithOverrides { overrides in
    overrides.toolThrew = { context in
      CallTool.Result(
        content: [
          .text("Weather machine failure: \(context.error.localizedDescription)")
        ],
        isError: true
      )
    }
  }
)

If you are happy with the toolkit's defaults, simply omit the messaging: argument.

Error Handling

The toolkit provides automatic error handling for tools. Any error thrown from call(with:) will be automatically converted to an error response with isError: true.

For custom error messages with structured content, throw a ToolError:

struct ValidatedTool: MCPTool {
  let name = "validated"

  @Schemable
  struct Parameters {
    let value: Int
  }

  func call(with arguments: Parameters) async throws(ToolError) -> Content {
    guard arguments.value > 0 else {
      throw ToolError {
        "Invalid input: value must be positive"
        "Received: \(arguments.value)"
        "Please provide a value greater than 0"
      }
    }

    return ["Success! Value is \(arguments.value)"]
  }
}

The ToolError supports the same @ToolContentBuilder syntax as the Content return type, allowing you to provide rich, multi-line error messages.

For simple single-line errors, use the convenience initializer:

throw ToolError("Value must be positive")

Running the Example Server with MCP Inspector

MCP Inspector is an interactive development tool for MCP servers.

To install MCP Inspector, run:

npm install -g @modelcontextprotocol/inspector

Then you can run the example cli with either stdio or HTTP transport modes.

Stdio

To run the example server with stdio transport, use:

npx @modelcontextprotocol/inspector@latest swift run MCPToolkitExample --transport stdio

This will start the server and connect it to MCP Inspector.

MCP Inspector screenshot (STDIO mode)

HTTP

In HTTP mode, the CLI will spin up a Vapor web server (on port 8080 by default) with MCP tools at /mcp endpoint.

First start the Vapor server:

swift run MCPToolkitExample --transport http

Then in another terminal, start MCP Inspector and connect to the server:

npx @modelcontextprotocol/inspector@latest --server-url http://127.0.0.1:8080/mcp --transport http

MCP Inspector screenshot (HTTP mode)

Resources

MCP Resources allow servers to expose data that clients can read. This is useful for providing context like documentation, configuration files, or dynamic content.

Defining a Resource

Conform to MCPResource and use the @ResourceContentBuilder to define your content declaratively:

import MCPToolkit

struct DocumentationResource: MCPResource {
  let uri = "docs://api/overview"
  let name: String? = "API Overview"
  let description: String? = "Complete API documentation"
  let mimeType: String? = "text/markdown"

  var content: Content {
    """
    # API Documentation

    Welcome to our API!
    """
  }
}

Multiple Content Blocks

Use Group to combine multiple strings with optional custom separators and MIME types:

struct HTMLPageResource: MCPResource {
  let uri = "ui://widget/page.html"
  let name: String? = "Widget Page"

  var content: Content {
    // HTML content with default newline separator
    Group {
      "<!DOCTYPE html>"
      "<html>"
      "<head><title>My Widget</title></head>"
      "<body><h1>Hello!</h1></body>"
      "</html>"
    }
    .mimeType("text/html")

    // CSS with custom separator
    Group(separator: " ") {
      ".widget { color: blue; }"
      ".title { font-size: 20px; }"
    }
    .mimeType("text/css")
  }
}

Binary Blobs

Resources can provide binary content (images, PDFs, etc.) as base64-encoded strings:

struct ImageResource: MCPResource {
  let uri = "data://images/logo.png"
  let name: String? = "Company Logo"

  var content: Content {
    // Provide base64-encoded binary data
    let base64PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
    ResourceContentItem.blob(base64PNG, mimeType: "image/png")
  }
}

// Mix text and binary content
struct DocumentWithImagesResource: MCPResource {
  let uri = "doc://report"

  var content: Content {
    Group {
      "# Monthly Report"
      "See the chart below."
    }
    .mimeType("text/markdown")

    // Embed a chart image
    ResourceContentItem.blob(chartImageBase64, mimeType: "image/png")

    Group {
      "## Summary"
      "Data shows positive trends."
    }
    .mimeType("text/markdown")
  }
}

Registering Resources

Register resources with your MCP server just like tools:

let server = Server(
  name: "Documentation Server",
  version: "1.0.0",
  capabilities: .init(resources: .init(listChanged: true))
)

await server.register(resources: [
  DocumentationResource(),
  HTMLPageResource(),
  ImageResource()
])

Documentation

Full API documentation is available on Swift Package Index here.

Installation

Swift Package Manager

Add swift-mcp-toolkit to your Package.swift:

dependencies: [
  .package(url: "https://github.com/ajevans99/swift-mcp-toolkit.git", from: "0.1.0")
]

Then add the dependency to your target:

.target(
  name: "YourTarget",
  dependencies: [
    .product(name: "MCPToolkit", package: "swift-mcp-toolkit")
  ]
)

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License

This project is licensed under the MIT License. See LICENSE for details.

Resources

About

Strongly typed MCP tools in Swift

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •