Skip to content

Commit 6ccae54

Browse files
committed
Make sure to properly deal with non-required parameters
1 parent 29b92a2 commit 6ccae54

File tree

5 files changed

+97
-90
lines changed

5 files changed

+97
-90
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// MCPToolMetadata+Arguments.swift
3+
// SwiftMCP
4+
//
5+
// Created by Oliver Drobnik on 28.03.25.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
Extension to provide utility methods for working with MCP tools.
12+
*/
13+
extension MCPToolMetadata {
14+
/**
15+
Enriches a dictionary of arguments with default values for any missing parameters.
16+
17+
- Parameters:
18+
- arguments: The original arguments dictionary
19+
20+
- Returns: A new dictionary with default values added for missing parameters
21+
- Throws: MCPToolError if required parameters are missing or if parameter conversion fails
22+
*/
23+
public func enrichArguments(_ arguments: [String: Sendable]) throws -> [String: Sendable] {
24+
// Create a copy of the arguments dictionary
25+
var enrichedArguments = arguments
26+
27+
// Check for missing required parameters and add default values
28+
for param in parameters {
29+
// If we don't have an argument for this parameter
30+
if enrichedArguments[param.name] == nil {
31+
// If it has a default value, use it
32+
if let defaultValue = param.defaultValue {
33+
enrichedArguments[param.name] = defaultValue
34+
}
35+
// If it's required and has no default value, throw an error
36+
else if param.isRequired {
37+
throw MCPToolError.missingRequiredParameter(parameterName: param.name)
38+
}
39+
}
40+
}
41+
42+
return enrichedArguments
43+
}
44+
}

Sources/SwiftMCP/Models/Tools/MCPTool.swift

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -62,54 +62,6 @@ extension MCPTool: Codable {
6262
}
6363
}
6464

65-
/**
66-
Extension to provide utility methods for working with MCP tools.
67-
*/
68-
extension MCPTool {
69-
/**
70-
Enriches a dictionary of arguments with default values for any missing parameters.
71-
72-
- Parameters:
73-
- arguments: The original arguments dictionary
74-
- object: The object containing the function metadata
75-
76-
- Returns: A new dictionary with default values added for missing parameters
77-
- Throws: MCPToolError if required parameters are missing or if parameter conversion fails
78-
*/
79-
public func enrichArguments(_ arguments: [String: Sendable], forObject object: Any) throws -> [String: Sendable] {
80-
// Use the provided function name or fall back to the tool's name
81-
let metadataKey = "__mcpMetadata_\(name)"
82-
83-
// Create a copy of the arguments dictionary
84-
var enrichedArguments = arguments
85-
86-
// Find the metadata for the function using reflection
87-
let mirror = Mirror(reflecting: object)
88-
guard let child = mirror.children.first(where: { $0.label == metadataKey }),
89-
let metadata = child.value as? MCPToolMetadata else {
90-
// If no metadata is found, return the original arguments
91-
return arguments
92-
}
93-
94-
// Check for missing required parameters
95-
for param in metadata.parameters {
96-
if enrichedArguments[param.name] == nil && param.defaultValue == nil {
97-
throw MCPToolError.missingRequiredParameter(parameterName: param.name)
98-
}
99-
}
100-
101-
// Add default values for parameters that are missing from the arguments dictionary
102-
for param in metadata.parameters {
103-
if enrichedArguments[param.name] == nil, let defaultValue = param.defaultValue {
104-
105-
enrichedArguments[param.name] = defaultValue
106-
}
107-
}
108-
109-
return enrichedArguments
110-
}
111-
}
112-
11365
/**
11466
Extension to make JSONSchema conform to Codable
11567
*/

Sources/SwiftMCP/Protocols/MCPServer.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,29 @@ public extension MCPServer {
465465
var mcpTools: [MCPTool] {
466466
return mcpToolMetadata.convertedToTools()
467467
}
468+
469+
// MARK: - Internal Helpers
470+
471+
/**
472+
Provides metadata for a function annotated with `@MCPTool`.
473+
474+
- Parameter toolName: The name of the tool
475+
- Returns: The metadata for the tool
476+
477+
This property uses runtime reflection to gather tool metadata from properties
478+
generated by the `@MCPTool` macro.
479+
*/
480+
func mcpToolMetadata(for toolName: String) -> MCPToolMetadata?
481+
{
482+
let metadataKey = "__mcpMetadata_\(toolName)"
483+
484+
// Find the metadata for the function using reflection
485+
let mirror = Mirror(reflecting: self)
486+
guard let child = mirror.children.first(where: { $0.label == metadataKey }),
487+
let metadata = child.value as? MCPToolMetadata else {
488+
return nil
489+
}
490+
491+
return metadata
492+
}
468493
}

Sources/SwiftMCPMacros/MCPServerMacro.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ public struct MCPServerMacro: MemberMacro, ExtensionMacro {
141141
/// - Returns: The result of the tool call
142142
/// - Throws: MCPToolError if the tool doesn't exist or cannot be called
143143
public func callTool(_ name: String, arguments: [String: Sendable]) async throws -> (Codable & Sendable) {
144-
// Find the tool by name
145-
guard let tool = mcpTools.first(where: { $0.name == name }) else {
144+
// Find the tool metadata by name
145+
guard let metadata = mcpToolMetadata(for: name) else {
146146
throw MCPToolError.unknownTool(name: name)
147147
}
148148
149149
// Enrich arguments with default values
150-
let enrichedArguments = try tool.enrichArguments(arguments, forObject: self)
150+
let enrichedArguments = try metadata.enrichArguments(arguments)
151151
152152
// Call the appropriate wrapper method based on the tool name
153153
switch name {

Tests/SwiftMCPTests/MCPToolArgumentEnrichingTests.swift

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ import SwiftMCP
1414
func testEnrichArguments() throws {
1515
let calculator = Calculator()
1616

17-
// Get the add tool from the calculator
18-
guard let tool = calculator.mcpTools.first(where: { $0.name == "add" }) else {
19-
throw TestError("Could not find add tool")
20-
}
17+
// Get the add tool metadata from the calculator
18+
let metadata = unwrap(calculator.mcpToolMetadata(for: "add"))
2119

2220
// Test enriching arguments
2321
let arguments: [String: Sendable] = ["a": 2, "b": 3]
24-
let enrichedArguments = try tool.enrichArguments(arguments, forObject: calculator as Any)
22+
let enrichedArguments = try metadata.enrichArguments(arguments)
2523

2624
// Check that the arguments were not changed
2725
#expect(enrichedArguments.count == 2)
@@ -33,14 +31,12 @@ func testEnrichArguments() throws {
3331
func testEnrichArgumentsWithExplicitFunctionName() throws {
3432
let calculator = Calculator()
3533

36-
// Get the add tool from the calculator
37-
guard let tool = calculator.mcpTools.first(where: { $0.name == "add" }) else {
38-
throw TestError("Could not find add tool")
39-
}
34+
// Get the add tool metadata from the calculator
35+
let metadata = unwrap(calculator.mcpToolMetadata(for: "add"))
4036

4137
// Test enriching arguments with explicit function name
4238
let arguments: [String: Sendable] = ["a": 2, "b": 3]
43-
let enrichedArguments = try tool.enrichArguments(arguments, forObject: calculator as Any)
39+
let enrichedArguments = try metadata.enrichArguments(arguments)
4440

4541
// Check that the arguments were not changed
4642
#expect(enrichedArguments.count == 2)
@@ -52,14 +48,12 @@ func testEnrichArgumentsWithExplicitFunctionName() throws {
5248
func testEnrichArgumentsWithNoDefaults() throws {
5349
let calculator = Calculator()
5450

55-
// Get a tool from the calculator
56-
guard let tool = calculator.mcpTools.first(where: { $0.name == "add" }) else {
57-
throw TestError("Could not find add tool")
58-
}
51+
// Get the add tool metadata from the calculator
52+
let metadata = unwrap(calculator.mcpToolMetadata(for: "add"))
5953

6054
// Test enriching arguments with no default values
6155
let arguments: [String: Sendable] = ["a": 2, "b": 3]
62-
let enrichedArguments = try tool.enrichArguments(arguments, forObject: calculator as Any)
56+
let enrichedArguments = try metadata.enrichArguments(arguments)
6357

6458
// Check that the arguments were not changed
6559
#expect(enrichedArguments.count == 2)
@@ -71,29 +65,25 @@ func testEnrichArgumentsWithNoDefaults() throws {
7165
func testEnrichArgumentsWithMissingRequiredArgument() throws {
7266
let calculator = Calculator()
7367

74-
// Get a tool from the calculator
75-
guard let tool = calculator.mcpTools.first(where: { $0.name == "add" }) else {
76-
throw TestError("Could not find add tool")
77-
}
68+
// Get the add tool metadata from the calculator
69+
let metadata = unwrap(calculator.mcpToolMetadata(for: "add"))
7870

7971
// Test enriching arguments with a missing required argument
8072
#expect(throws: MCPToolError.self, "Should notice missing parameter") {
81-
try tool.enrichArguments(["a": 2], forObject: calculator)
73+
try metadata.enrichArguments(["a": 2])
8274
}
8375
}
8476

8577
@Test
8678
func testEnrichArgumentsWithTypeConversion() throws {
8779
let calculator = Calculator()
8880

89-
// Get a tool from the calculator
90-
guard let tool = calculator.mcpTools.first(where: { $0.name == "add" }) else {
91-
throw TestError("Could not find add tool")
92-
}
81+
// Get the add tool metadata from the calculator
82+
let metadata = unwrap(calculator.mcpToolMetadata(for: "add"))
9383

9484
// Test enriching arguments with string values that need to be converted
9585
let arguments: [String: Sendable] = ["a": "2", "b": "3"]
96-
let enrichedArguments = try tool.enrichArguments(arguments, forObject: calculator as Any)
86+
let enrichedArguments = try metadata.enrichArguments(arguments)
9787

9888
// Check that the arguments were not changed (enrichArguments doesn't do type conversion)
9989
#expect(enrichedArguments.count == 2)
@@ -105,23 +95,21 @@ func testEnrichArgumentsWithTypeConversion() throws {
10595
func testSubtractArguments() throws {
10696
let calculator = Calculator()
10797

108-
// Get a tool from the calculator
109-
guard let tool = calculator.mcpTools.first(where: { $0.name == "subtract" }) else {
110-
throw TestError("Could not find subtract tool")
111-
}
98+
// Get the subtract tool metadata from the calculator
99+
let metadata = unwrap(calculator.mcpToolMetadata(for: "subtract"))
112100

113101
// Test with no arguments - should throw missing required parameter
114102
#expect(throws: MCPToolError.self, "Should notice missing parameter") {
115-
try tool.enrichArguments([:], forObject: calculator)
103+
try metadata.enrichArguments([:])
116104
}
117105

118106
// Test with partial arguments - should throw missing required parameter
119107
#expect(throws: MCPToolError.self, "Should notice missing parameter") {
120-
try tool.enrichArguments(["b": 5], forObject: calculator)
108+
try metadata.enrichArguments(["b": 5])
121109
}
122110

123111
// Test with all arguments - no defaults should be added
124-
let allArgs = try tool.enrichArguments(["a": 20, "b": 5], forObject: calculator)
112+
let allArgs = try metadata.enrichArguments(["a": 20, "b": 5])
125113
#expect(allArgs.count == 2)
126114
#expect(allArgs["a"] as? Int == 20)
127115
#expect(allArgs["b"] as? Int == 5)
@@ -131,23 +119,21 @@ func testSubtractArguments() throws {
131119
func testMultiplyArguments() throws {
132120
let calculator = Calculator()
133121

134-
// Get a tool from the calculator
135-
guard let tool = calculator.mcpTools.first(where: { $0.name == "multiply" }) else {
136-
throw TestError("Could not find multiply tool")
137-
}
122+
// Get the multiply tool metadata from the calculator
123+
let metadata = unwrap(calculator.mcpToolMetadata(for: "multiply"))
138124

139125
// Test with no arguments - should throw missing required parameter
140126
#expect(throws: MCPToolError.self, "Should notice missing parameter") {
141-
try tool.enrichArguments([:], forObject: calculator)
127+
try metadata.enrichArguments([:])
142128
}
143129

144130
// Test with partial arguments - should throw missing required parameter
145131
#expect(throws: MCPToolError.self, "Should notice missing parameter") {
146-
try tool.enrichArguments(["b": 5], forObject: calculator)
132+
try metadata.enrichArguments(["b": 5])
147133
}
148134

149135
// Test with all arguments - no defaults should be added
150-
let allArgs = try tool.enrichArguments(["a": 20, "b": 5], forObject: calculator)
136+
let allArgs = try metadata.enrichArguments(["a": 20, "b": 5])
151137
#expect(allArgs.count == 2)
152138
#expect(allArgs["a"] as? Int == 20)
153139
#expect(allArgs["b"] as? Int == 5)

0 commit comments

Comments
 (0)