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/inspectorThen 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 stdioThis 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 httpThen 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