Contents

ajevans99/swift-mcp-toolkit

Strongly typed MCP tools in Swift

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."
    }
  }
}

<details> <summary>Compare to see the vanilla <code>swift-sdk</code> approach</summary>

// 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)])
    }
  }
}

</details>

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

Package Metadata

Repository: ajevans99/swift-mcp-toolkit

Stars: 10

Forks: 4

Open issues: 3

Default branch: main

Primary language: swift

License: MIT

README: README.md