Skip to content

Schema-Based Tool Registration and Handling #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"originHash" : "cce36cb33302c2f1c28458e19b8439f736fc28106e4c6ea95d7992c74594c242",
"originHash" : "8375176f0e64ba7d128bf657147c408dc96035d49440183a1f643c94e3f77122",
"pins" : [
{
"identity" : "swift-json-schema",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ajevans99/swift-json-schema",
"state" : {
"revision" : "2ba78e486722955e0147574fb6806027abb29faa",
"version" : "0.4.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
Expand All @@ -10,6 +19,15 @@
"version" : "1.6.2"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
Expand Down
43 changes: 34 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ import PackageDescription
let package = Package(
name: "mcp-swift-sdk",
platforms: [
.macOS("13.0"),
.macCatalyst("16.0"),
.iOS("16.0"),
.watchOS("9.0"),
.tvOS("16.0"),
.visionOS("1.0"),
.macOS(.v14),
.macCatalyst(.v17),
.iOS(.v17),
.watchOS(.v10),
.tvOS(.v17),
.visionOS(.v1),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MCP",
targets: ["MCP"])
targets: ["MCP", "SchemaMCP"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://github.com/ajevans99/swift-json-schema", from: "0.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand All @@ -31,13 +33,36 @@ let package = Package(
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
]),
],
path: "Sources/MCP"
),
.testTarget(
name: "MCPTests",
dependencies: [
"MCP",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
]),
],
path: "Tests/MCPTests"
),
.target(
name: "SchemaMCP",
dependencies: [
"MCP",
.product(name: "JSONSchema", package: "swift-json-schema"),
.product(name: "JSONSchemaBuilder", package: "swift-json-schema"),
],
path: "SchemaMCP/Sources"
),
.testTarget(
name: "SchemaMCPTests",
dependencies: [
"MCP",
"SchemaMCP",
.product(name: "JSONSchema", package: "swift-json-schema"),
.product(name: "JSONSchemaBuilder", package: "swift-json-schema"),
],
path: "SchemaMCP/Tests"
),
]
)
153 changes: 153 additions & 0 deletions SchemaMCP/Sources/SchemaTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Foundation
import JSONSchema
import JSONSchemaBuilder
import MCP

public extension Server {
/// Registers a toolbox of tools with the server.
/// - Parameter toolBox: The toolbox to register.
/// - Returns: The server instance for chaining.
@discardableResult
func withTools<each S>(
_ toolBox: ToolBox< repeat each S>
) -> Self {
withMethodHandler(ListTools.self) { parameters in
try .init(tools: toolBox.mcpTools(), nextCursor: parameters.cursor)
}

return withMethodHandler(CallTool.self) { call in
for tool in repeat each toolBox.tools {
if call.name == tool.name {
let result = try await tool.handler(call.arguments)
return result
}
}

return .init(content: [.text("Tool `\(call.name)` not found")], isError: true)
}
}
}

/// A toolbox holding a variadic list of schema tools.
public struct ToolBox<each T: Schemable>: Sendable {
/// The tuple of tools.
public let tools: (repeat SchemaTool<each T>)

/// Initializes a toolbox with the given tools.
/// - Parameter tools: The tuple of tools.
public init(tools: (repeat SchemaTool<each T>)) {
self.tools = tools
}

/// Converts all tools to MCP tool definitions.
/// - Throws: `MCPError` if any conversion fails.
public func mcpTools() throws(MCPError) -> [Tool] {
var mcpTools: [Tool] = []
for tool in repeat (each tools) {
try mcpTools.append(tool.toMCPTool())
}
return mcpTools
}
}

/// Represents a tool with a schema-based input and async handler.
public struct SchemaTool<Schema: Schemable>: Identifiable, Sendable {
/// The tool name.
public let name: String
/// The tool description.
public let description: String
/// The tool input schema type.
public let inputType: Schema.Type
/// The tool handler.
private let handlerClosure: @Sendable (Schema) async throws -> CallTool.Result
/// Schema used to parse or validate input.
private let inputSchema: Schema.Schema

/// The tool's unique identifier (same as name).
public var id: String { name }

/// Parses arguments into the schema type.
/// - Parameter arguments: The arguments to parse.
/// - Throws: `MCPError.parseError` if parsing fails or type mismatch.
public func parse(_ arguments: [String: Value]?) throws(MCPError) -> Schema {
let output = try inputSchema.parse(arguments)
guard let schema = output as? Schema else {
throw MCPError.parseError("Schema.Schema.Output != Schema")
}
return schema
}

/// Handles a tool call with arguments.
/// - Parameter arguments: The arguments to handle.
/// - Returns: The result of the tool call.
public func handler(
_ arguments: [String: Value]?
) async throws -> CallTool.Result {
do {
let schema = try parse(arguments)
return try await handlerClosure(schema)
} catch let error as MCPError {
return .init(content: [.text("Invalid arguments: \(error)")], isError: true)
}
}

/// Initializes a new `SchemaTool`.
/// - Parameters:
/// - name: The tool name.
/// - description: The tool description.
/// - inputType: The schema type for input.
/// - handler: The async handler closure.
public init(
name: String,
description: String,
inputType: Schema.Type,
handler: @escaping @Sendable (Schema) async throws -> CallTool.Result
) {
self.name = name
self.description = description
self.inputType = inputType
handlerClosure = handler
inputSchema = inputType.schema
}

/// Converts the tool to an MCP tool definition.
/// - Throws: `MCPError` if conversion fails.
public func toMCPTool() throws(MCPError) -> Tool {
try .init(
name: name,
description: description,
inputSchema: .init(schema: inputSchema)
)
}
}

/// Extension to initialize `Value` from a JSONSchemaComponent.
public extension Value {
/// Initializes a `Value` from a schema component.
/// - Parameter schema: The schema component to encode.
/// - Throws: `MCPError.parseError` if encoding or decoding fails.
init(schema: some JSONSchemaComponent) throws(MCPError) {
do {
let data = try JSONEncoder().encode(schema.definition())
self = try JSONDecoder().decode(Value.self, from: data)
} catch {
throw MCPError.parseError("\(error)")
}
}
}

/// Extension to parse arguments using a JSONSchemaComponent.
public extension JSONSchemaComponent {
/// Parses and validates arguments using the schema.
/// - Parameter arguments: The arguments to parse.
/// - Throws: `MCPError.invalidParams` if parsing fails.
func parse(_ arguments: [String: Value]?) throws(MCPError) -> Output {
do {
let data = try JSONEncoder().encode(arguments)
let string = String(data: data, encoding: .utf8) ?? ""
return try parseAndValidate(instance: string)
} catch {
throw MCPError.invalidParams("\(error)")
}
}
}
Loading