Skip to content
This repository was archived by the owner on May 29, 2025. It is now read-only.

Commit a436420

Browse files
authored
fix issue with calling tool, and add test (#8)
1 parent ca552ec commit a436420

File tree

17 files changed

+490
-83
lines changed

17 files changed

+490
-83
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
import AppKit
3+
import JSONSchemaBuilder
4+
import MCPServer
5+
6+
// MARK: - EmptyInput
7+
8+
@Schemable
9+
struct EmptyInput { }
10+
11+
let testTool = Tool(name: "test") { (_: EmptyInput) async throws in
12+
[]
13+
}

ExampleMCPServer/Sources/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ let server = try await MCPServer(
3737
Tool(name: "repeat") { (input: RepeatToolInput) in
3838
[.text(.init(text: input.text))]
3939
},
40+
testTool,
4041
]),
4142
transport: proxy(transport))
4243

MCPClient/Sources/MCPClient.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public actor MCPClient: MCPClientInterface {
8282
async throws -> CallToolResult
8383
{
8484
guard serverInfo.capabilities.tools != nil else {
85-
throw MCPError.notSupported
85+
throw MCPError.capabilityNotSupported
8686
}
8787
var progressToken: String? = nil
8888
if let progressHandler {
@@ -107,14 +107,14 @@ public actor MCPClient: MCPClientInterface {
107107

108108
public func getPrompt(named name: String, arguments: JSON? = nil) async throws -> GetPromptResult {
109109
guard serverInfo.capabilities.prompts != nil else {
110-
throw MCPError.notSupported
110+
throw MCPError.capabilityNotSupported
111111
}
112112
return try await connection.getPrompt(.init(name: name, arguments: arguments))
113113
}
114114

115115
public func readResource(uri: String) async throws -> ReadResourceResult {
116116
guard serverInfo.capabilities.resources != nil else {
117-
throw MCPError.notSupported
117+
throw MCPError.capabilityNotSupported
118118
}
119119
return try await connection.readResource(.init(uri: uri))
120120
}
@@ -137,7 +137,7 @@ public actor MCPClient: MCPClientInterface {
137137
private static func connectToServer(connection: MCPClientConnectionInterface) async throws -> ServerInfo {
138138
let response = try await connection.initialize()
139139
guard response.protocolVersion == MCP.protocolVersion else {
140-
throw MCPClientError.versionMismatch
140+
throw MCPClientError.versionMismatch(received: response.protocolVersion, expected: MCP.protocolVersion)
141141
}
142142

143143
try await connection.acknowledgeInitialization()

MCPClient/Sources/MCPClientInterface.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Foundation
12
import JSONRPC
23
import MCPInterface
34
import MemberwiseInit
@@ -53,6 +54,20 @@ public struct ClientCapabilityHandlers {
5354
// MARK: - MCPClientError
5455

5556
public enum MCPClientError: Error {
56-
case versionMismatch
57+
case versionMismatch(received: String, expected: String)
5758
case toolCallError(executionErrors: [CallToolResult.ExecutionError])
5859
}
60+
61+
// MARK: LocalizedError
62+
63+
extension MCPClientError: LocalizedError {
64+
65+
public var errorDescription: String? {
66+
switch self {
67+
case .versionMismatch(let received, let expected):
68+
return "Version mismatch between server and client. Received: \(received), Expected: \(expected)"
69+
case .toolCallError(let executionErrors):
70+
return "Error executing tool:\n\(executionErrors.map { $0.errorDescription ?? "unknown error" }.joined(separator: "\n\n"))"
71+
}
72+
}
73+
}

MCPClient/Sources/MockMCPConnection.swift renamed to MCPClient/Tests/MockMCPClientConnection.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11

2+
import MCPClient
23
import MCPInterface
34

4-
#if DEBUG
5-
// TODO: move to a test helper package
5+
// MARK: - MockMCPClientConnection
66

77
/// A mock `MCPClientConnection` that can be used in tests.
88
class MockMCPClientConnection: MCPClientConnectionInterface {
@@ -202,7 +202,8 @@ class MockMCPClientConnection: MCPClientConnectionInterface {
202202

203203
}
204204

205+
// MARK: - MockMCPClientConnectionError
206+
205207
enum MockMCPClientConnectionError: Error {
206208
case notImplemented(function: String)
207209
}
208-
#endif

MCPInterface/Sources/Interfaces.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Foundation
12
import JSONRPC
23
import MemberwiseInit
34

@@ -50,15 +51,27 @@ extension CapabilityStatus {
5051
case .supported(let capability):
5152
return capability
5253
case .notSupported:
53-
throw MCPError.notSupported
54+
throw MCPError.capabilityNotSupported
5455
}
5556
}
5657
}
5758

5859
// MARK: - MCPError
5960

6061
public enum MCPError: Error {
61-
case notSupported
62+
case capabilityNotSupported
63+
}
64+
65+
// MARK: LocalizedError
66+
67+
extension MCPError: LocalizedError {
68+
69+
public var errorDescription: String? {
70+
switch self {
71+
case .capabilityNotSupported:
72+
return "The requested capability is not supported"
73+
}
74+
}
6275
}
6376

6477
public typealias HandleServerRequest = (ServerRequest, (AnyJRPCResponse) -> Void)

MCPInterface/Sources/mcp_interfaces/Interface+extensions.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,3 +642,30 @@ extension PromptReference {
642642
name = try container.decode(String.self, forKey: "name")
643643
}
644644
}
645+
646+
// MARK: - CallToolResult.ExecutionError + LocalizedError
647+
648+
extension CallToolResult.ExecutionError: LocalizedError {
649+
650+
public var errorDescription: String? {
651+
text
652+
}
653+
}
654+
655+
// MARK: - JRPCError + LocalizedError
656+
657+
extension JRPCError: LocalizedError {
658+
659+
public var errorDescription: String? {
660+
if let data {
661+
do {
662+
if let dataStr = String(data: try JSONEncoder().encode(data), encoding: .utf8) {
663+
return "JRPC error \(code): \(message)\n\(dataStr)"
664+
}
665+
} catch {
666+
// will fall back to the default error description
667+
}
668+
}
669+
return "JRPC error \(code): \(message)"
670+
}
671+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import JSONSchema
2+
3+
typealias JSONSchema_JSONValue = JSONSchema.JSONValue

MCPServer/Sources/Convenience/Schemable+extensions.swift

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import JSONRPC
23
import JSONSchema
34
import JSONSchemaBuilder
45
import MCPInterface
@@ -17,7 +18,7 @@ import MCPInterface
1718

1819
/// Definition for a tool the client can call.
1920
public protocol CallableTool {
20-
associatedtype Input: Decodable
21+
associatedtype Input
2122
/// A JSON Schema object defining the expected parameters for the tool.
2223
var inputSchema: JSON { get }
2324
/// The name of the tool.
@@ -85,44 +86,22 @@ extension Tool where Input: Schemable {
8586
description: description,
8687
inputSchema: Input.schema.schemaValue.json,
8788
decodeInput: { data in
88-
let json = try JSONDecoder().decode(JSONValue.self, from: data)
89+
let json = try JSONDecoder().decode(JSONSchema_JSONValue.self, from: data)
90+
8991
switch Input.schema.parse(json) {
9092
case .valid(let value):
9193
return value
92-
case .invalid(let errors):
93-
throw errors.first ?? MCPServerError.toolCallError(errors)
94+
case .invalid:
95+
throw MCPServerError.decodingError(input: data, schema: Input.schema.schemaValue.json)
9496
}
9597
},
9698
call: call)
9799
}
98100
}
99101

100-
extension Tool where Input: Decodable {
101-
public init(
102-
name: String,
103-
description: String? = nil,
104-
inputSchema: JSON,
105-
call: @escaping (Input) async throws -> [TextContentOrImageContentOrEmbeddedResource])
106-
{
107-
self.init(
108-
name: name,
109-
description: description,
110-
inputSchema: inputSchema,
111-
decodeInput: { data in
112-
try JSONDecoder().decode(Input.self, from: data)
113-
},
114-
call: call)
115-
}
116-
}
117-
118102
extension CallableTool {
119-
public func decodeInput(_ input: JSON?) throws -> Input {
120-
let data = try JSONEncoder().encode(input)
121-
return try JSONDecoder().decode(Input.self, from: data)
122-
}
123-
124-
public func call(_ input: JSON?) async throws -> [TextContentOrImageContentOrEmbeddedResource] {
125-
let input = try decodeInput(input)
103+
public func call(json: JSON?) async throws -> [TextContentOrImageContentOrEmbeddedResource] {
104+
let input: Input = try decodeInput(json)
126105
return try await call(input)
127106
}
128107
}
@@ -138,11 +117,13 @@ extension Array where Element == any CallableTool {
138117
handler: { request in
139118
let name = request.name
140119
guard let tool = toolsByName[name] else {
141-
throw MCPError.notSupported
120+
throw JSONRPCResponseError<JSONRPC.JSONValue>(
121+
code: JRPCErrorCodes.invalidParams.rawValue,
122+
message: "Unknown tool: \(name)")
142123
}
143124
let arguments = request.arguments
144125
do {
145-
let content = try await tool.call(arguments)
126+
let content = try await tool.call(json: arguments)
146127
return CallToolResult(content: content)
147128
} catch {
148129
return CallToolResult(content: [.text(.init(text: error.localizedDescription))], isError: true)
@@ -158,13 +139,13 @@ extension Array where Element == any CallableTool {
158139
}
159140

160141
/// Convert between the JSON representation from `JSONSchema` and ours
161-
extension [KeywordIdentifier: JSONValue] {
142+
extension [KeywordIdentifier: JSONSchema_JSONValue] {
162143
fileprivate var json: JSON {
163144
.object(mapValues { $0.value })
164145
}
165146
}
166147

167-
extension JSONValue {
148+
extension JSONSchema_JSONValue {
168149
fileprivate var value: JSON.Value {
169150
switch self {
170151
case .null:
@@ -184,7 +165,3 @@ extension JSONValue {
184165
}
185166
}
186167
}
187-
188-
// MARK: - ParseIssue + Error
189-
190-
extension ParseIssue: @retroactive Error { }

0 commit comments

Comments
 (0)