modality-mcp-kit 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FastHonoMcp.js +5 -16
- package/dist/handlers/tools-call-handler.js +41 -0
- package/dist/types/handlers/tools-call-handler.d.ts +30 -0
- package/dist/types/mcp-result-types.js +259 -0
- package/dist/types/types/mcp-result-types.d.ts +67 -0
- package/dist/types/utils/normalize-tool-result.d.ts +41 -0
- package/dist/utils/normalize-tool-result.js +120 -0
- package/package.json +1 -1
package/dist/FastHonoMcp.js
CHANGED
|
@@ -27,7 +27,9 @@ import { ModalityFastMCP } from "modality-mcp-kit";
|
|
|
27
27
|
import { JSONRPCManager, getLoggerInstance, } from "modality-kit";
|
|
28
28
|
import { sseNotification, sseError, SSE_HEADERS, createSSEStream, } from "./sse-wrapper.js";
|
|
29
29
|
import { McpSessionManager } from "./McpSessionManager.js";
|
|
30
|
+
import { handleToolCall } from "./handlers/tools-call-handler.js";
|
|
30
31
|
const defaultMcpPath = "/mcp";
|
|
32
|
+
const mcpSchemaVersion = "2025-11-25";
|
|
31
33
|
// Initialize FastMCP instance for internal use (NO SERVER)
|
|
32
34
|
export class FastHonoMcp extends ModalityFastMCP {
|
|
33
35
|
logger;
|
|
@@ -114,7 +116,7 @@ export class FastHonoMcp extends ModalityFastMCP {
|
|
|
114
116
|
writer.send(result);
|
|
115
117
|
}, headers);
|
|
116
118
|
}
|
|
117
|
-
return c.json({ error:
|
|
119
|
+
return c.json({ error: `Use ${this.mcpPath} for MCP requests` }, 400);
|
|
118
120
|
}
|
|
119
121
|
catch (error) {
|
|
120
122
|
this.logger.error(`FastHonoMcp (${url.pathname}) Middleware Error`, error);
|
|
@@ -154,7 +156,7 @@ function createJsonRpcManager(middleware) {
|
|
|
154
156
|
}
|
|
155
157
|
// Return valid InitializeResult
|
|
156
158
|
return {
|
|
157
|
-
protocolVersion:
|
|
159
|
+
protocolVersion: mcpSchemaVersion,
|
|
158
160
|
capabilities: {
|
|
159
161
|
tools: { listChanged: true },
|
|
160
162
|
...(mcpPrompts.length > 0 && { prompts: { listChanged: true } }),
|
|
@@ -183,20 +185,7 @@ function createJsonRpcManager(middleware) {
|
|
|
183
185
|
});
|
|
184
186
|
jsonrpc.registerMethod("tools/call", {
|
|
185
187
|
async handler(params) {
|
|
186
|
-
|
|
187
|
-
const { name, arguments: args } = params;
|
|
188
|
-
const tool = mcpTools.find((t) => t.name === name);
|
|
189
|
-
if (!tool) {
|
|
190
|
-
throw new ERROR_METHOD_NOT_FOUND(`Tool not found: ${name}`);
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
content: [
|
|
194
|
-
{
|
|
195
|
-
text: await tool.execute(args),
|
|
196
|
-
type: "text",
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
};
|
|
188
|
+
return handleToolCall(params, mcpTools);
|
|
200
189
|
},
|
|
201
190
|
});
|
|
202
191
|
jsonrpc.registerMethod("prompts/list", {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools/Call Handler
|
|
3
|
+
*
|
|
4
|
+
* Independent MCP JSON-RPC handler for tools/call method
|
|
5
|
+
* Manages tool lookup, execution, result normalization, and error handling
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Type-safe tool lookup with proper error handling
|
|
9
|
+
* - Rich result support (multiple content types, structured data, errors)
|
|
10
|
+
* - Exception-to-isError conversion
|
|
11
|
+
* - Backward compatible with string-returning tools
|
|
12
|
+
*/
|
|
13
|
+
import { normalizeToolResult, normalizeToolError, } from "../utils/normalize-tool-result.js";
|
|
14
|
+
/**
|
|
15
|
+
* Handles the tools/call JSON-RPC method
|
|
16
|
+
*
|
|
17
|
+
* @param params - Tool call parameters (name and arguments)
|
|
18
|
+
* @param mcpTools - List of available tools
|
|
19
|
+
* @returns CallToolResult with content, optional structured data, and error flag
|
|
20
|
+
* @throws ERROR_METHOD_NOT_FOUND if tool not found (converted to isError in caller)
|
|
21
|
+
*/
|
|
22
|
+
export async function handleToolCall(params, mcpTools) {
|
|
23
|
+
const { ERROR_METHOD_NOT_FOUND } = await import("modality-kit");
|
|
24
|
+
const { name, arguments: args } = params;
|
|
25
|
+
// Find the requested tool
|
|
26
|
+
const tool = mcpTools.find((t) => t.name === name);
|
|
27
|
+
if (!tool) {
|
|
28
|
+
throw new ERROR_METHOD_NOT_FOUND(`Tool not found: ${name}`);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
// Execute the tool with provided arguments
|
|
32
|
+
const result = await tool.execute(args || {});
|
|
33
|
+
// Normalize result to CallToolResult format
|
|
34
|
+
return normalizeToolResult(result);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// Convert exceptions to isError response
|
|
38
|
+
// This ensures errors don't break the protocol, following MCP spec guidance
|
|
39
|
+
return normalizeToolError(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools/Call Handler
|
|
3
|
+
*
|
|
4
|
+
* Independent MCP JSON-RPC handler for tools/call method
|
|
5
|
+
* Manages tool lookup, execution, result normalization, and error handling
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Type-safe tool lookup with proper error handling
|
|
9
|
+
* - Rich result support (multiple content types, structured data, errors)
|
|
10
|
+
* - Exception-to-isError conversion
|
|
11
|
+
* - Backward compatible with string-returning tools
|
|
12
|
+
*/
|
|
13
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/spec.types.js";
|
|
14
|
+
import type { FastMCPTool } from "../schemas/schemas_tool_config.js";
|
|
15
|
+
/**
|
|
16
|
+
* Parameters for tools/call JSON-RPC method
|
|
17
|
+
*/
|
|
18
|
+
export interface ToolCallParams {
|
|
19
|
+
name: string;
|
|
20
|
+
arguments?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handles the tools/call JSON-RPC method
|
|
24
|
+
*
|
|
25
|
+
* @param params - Tool call parameters (name and arguments)
|
|
26
|
+
* @param mcpTools - List of available tools
|
|
27
|
+
* @returns CallToolResult with content, optional structured data, and error flag
|
|
28
|
+
* @throws ERROR_METHOD_NOT_FOUND if tool not found (converted to isError in caller)
|
|
29
|
+
*/
|
|
30
|
+
export declare function handleToolCall(params: ToolCallParams, mcpTools: FastMCPTool<any, any>[]): Promise<CallToolResult>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Result Types and Validators
|
|
3
|
+
*
|
|
4
|
+
* Provides type definitions and validation utilities for MCP CallToolResult
|
|
5
|
+
* schema compliance. Supports multiple content types, structured data,
|
|
6
|
+
* error handling, and metadata.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Type guards for result type detection
|
|
10
|
+
*/
|
|
11
|
+
export function isString(value) {
|
|
12
|
+
return typeof value === "string";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if value looks like it's intended to be a CallToolResult
|
|
16
|
+
* (has content array field, regardless of validity)
|
|
17
|
+
*/
|
|
18
|
+
export function looksLikeCallToolResult(value) {
|
|
19
|
+
if (typeof value !== "object" || value === null) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const obj = value;
|
|
23
|
+
// If it has a content array field, it's intended to be CallToolResult
|
|
24
|
+
return Array.isArray(obj.content);
|
|
25
|
+
}
|
|
26
|
+
export function isCallToolResult(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const obj = value;
|
|
31
|
+
return (Array.isArray(obj.content) &&
|
|
32
|
+
obj.content.length > 0 &&
|
|
33
|
+
(typeof obj.isError === "undefined" || typeof obj.isError === "boolean") &&
|
|
34
|
+
(typeof obj.structuredContent === "undefined" ||
|
|
35
|
+
typeof obj.structuredContent === "object"));
|
|
36
|
+
}
|
|
37
|
+
export function isPlainObject(value) {
|
|
38
|
+
return (typeof value === "object" &&
|
|
39
|
+
value !== null &&
|
|
40
|
+
!Array.isArray(value) &&
|
|
41
|
+
!isCallToolResult(value));
|
|
42
|
+
}
|
|
43
|
+
export function isErrorResult(value) {
|
|
44
|
+
return isCallToolResult(value) && value.isError === true;
|
|
45
|
+
}
|
|
46
|
+
export function isNullOrUndefined(value) {
|
|
47
|
+
return value === null || value === undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Type-safe ContentBlock validators
|
|
51
|
+
*/
|
|
52
|
+
export function isTextContent(block) {
|
|
53
|
+
if (typeof block !== "object" || block === null)
|
|
54
|
+
return false;
|
|
55
|
+
const obj = block;
|
|
56
|
+
return obj.type === "text" && typeof obj.text === "string";
|
|
57
|
+
}
|
|
58
|
+
export function isImageContent(block) {
|
|
59
|
+
if (typeof block !== "object" || block === null)
|
|
60
|
+
return false;
|
|
61
|
+
const obj = block;
|
|
62
|
+
return (obj.type === "image" &&
|
|
63
|
+
typeof obj.data === "string" &&
|
|
64
|
+
typeof obj.mimeType === "string");
|
|
65
|
+
}
|
|
66
|
+
export function isAudioContent(block) {
|
|
67
|
+
if (typeof block !== "object" || block === null)
|
|
68
|
+
return false;
|
|
69
|
+
const obj = block;
|
|
70
|
+
return (obj.type === "audio" &&
|
|
71
|
+
typeof obj.data === "string" &&
|
|
72
|
+
typeof obj.mimeType === "string");
|
|
73
|
+
}
|
|
74
|
+
export function isResourceLink(block) {
|
|
75
|
+
if (typeof block !== "object" || block === null)
|
|
76
|
+
return false;
|
|
77
|
+
const obj = block;
|
|
78
|
+
return obj.type === "resource_link";
|
|
79
|
+
}
|
|
80
|
+
export function isEmbeddedResource(block) {
|
|
81
|
+
if (typeof block !== "object" || block === null)
|
|
82
|
+
return false;
|
|
83
|
+
const obj = block;
|
|
84
|
+
return obj.type === "resource" && typeof obj.resource === "object";
|
|
85
|
+
}
|
|
86
|
+
export function isContentBlock(block) {
|
|
87
|
+
return (isTextContent(block) ||
|
|
88
|
+
isImageContent(block) ||
|
|
89
|
+
isAudioContent(block) ||
|
|
90
|
+
isResourceLink(block) ||
|
|
91
|
+
isEmbeddedResource(block));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Validates a single ContentBlock against MCP schema
|
|
95
|
+
*/
|
|
96
|
+
export function validateContentBlock(block) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
if (typeof block !== "object" || block === null) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
errors: [{ field: "block", message: "Content block must be an object" }],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const obj = block;
|
|
105
|
+
const type = obj.type;
|
|
106
|
+
// Validate type field exists
|
|
107
|
+
if (typeof type !== "string") {
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
errors: [{ field: "type", message: "type field is required and must be a string" }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Validate based on type
|
|
114
|
+
switch (type) {
|
|
115
|
+
case "text": {
|
|
116
|
+
if (typeof obj.text !== "string") {
|
|
117
|
+
errors.push({
|
|
118
|
+
field: "text",
|
|
119
|
+
message: "text field is required and must be a string",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "image": {
|
|
125
|
+
if (typeof obj.data !== "string") {
|
|
126
|
+
errors.push({
|
|
127
|
+
field: "data",
|
|
128
|
+
message: "data field is required and must be a base64 string",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (typeof obj.mimeType !== "string") {
|
|
132
|
+
errors.push({
|
|
133
|
+
field: "mimeType",
|
|
134
|
+
message: "mimeType field is required for image content",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "audio": {
|
|
140
|
+
if (typeof obj.data !== "string") {
|
|
141
|
+
errors.push({
|
|
142
|
+
field: "data",
|
|
143
|
+
message: "data field is required and must be a base64 string",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (typeof obj.mimeType !== "string") {
|
|
147
|
+
errors.push({
|
|
148
|
+
field: "mimeType",
|
|
149
|
+
message: "mimeType field is required for audio content",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "resource_link":
|
|
155
|
+
case "resource": {
|
|
156
|
+
// More lenient validation for resource types
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
160
|
+
errors.push({
|
|
161
|
+
field: "type",
|
|
162
|
+
message: `Invalid content type: ${type}. Must be one of: text, image, audio, resource_link, resource`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
valid: errors.length === 0,
|
|
168
|
+
errors,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Validates a complete CallToolResult against MCP schema
|
|
173
|
+
*/
|
|
174
|
+
export function validateCallToolResult(result) {
|
|
175
|
+
const errors = [];
|
|
176
|
+
if (typeof result !== "object" || result === null) {
|
|
177
|
+
return {
|
|
178
|
+
valid: false,
|
|
179
|
+
errors: [
|
|
180
|
+
{
|
|
181
|
+
field: "result",
|
|
182
|
+
message: "CallToolResult must be an object",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const obj = result;
|
|
188
|
+
// Validate content array
|
|
189
|
+
if (!Array.isArray(obj.content)) {
|
|
190
|
+
errors.push({
|
|
191
|
+
field: "content",
|
|
192
|
+
message: "content field is required and must be an array",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (obj.content.length === 0) {
|
|
196
|
+
errors.push({
|
|
197
|
+
field: "content",
|
|
198
|
+
message: "content array cannot be empty",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Validate each content block
|
|
203
|
+
obj.content.forEach((block, index) => {
|
|
204
|
+
const blockValidation = validateContentBlock(block);
|
|
205
|
+
if (!blockValidation.valid) {
|
|
206
|
+
blockValidation.errors.forEach((err) => {
|
|
207
|
+
errors.push({
|
|
208
|
+
field: `content[${index}].${err.field}`,
|
|
209
|
+
message: err.message,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Validate optional fields
|
|
216
|
+
if (typeof obj.isError !== "undefined" &&
|
|
217
|
+
typeof obj.isError !== "boolean") {
|
|
218
|
+
errors.push({
|
|
219
|
+
field: "isError",
|
|
220
|
+
message: "isError field must be a boolean if provided",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (typeof obj.structuredContent !== "undefined" &&
|
|
224
|
+
(typeof obj.structuredContent !== "object" ||
|
|
225
|
+
obj.structuredContent === null ||
|
|
226
|
+
Array.isArray(obj.structuredContent))) {
|
|
227
|
+
errors.push({
|
|
228
|
+
field: "structuredContent",
|
|
229
|
+
message: "structuredContent field must be an object if provided",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
valid: errors.length === 0,
|
|
234
|
+
errors,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Creates a TextContent block
|
|
239
|
+
*/
|
|
240
|
+
export function createTextContent(text, annotations, _meta) {
|
|
241
|
+
const content = {
|
|
242
|
+
type: "text",
|
|
243
|
+
text,
|
|
244
|
+
};
|
|
245
|
+
if (annotations)
|
|
246
|
+
content.annotations = annotations;
|
|
247
|
+
if (_meta)
|
|
248
|
+
content._meta = _meta;
|
|
249
|
+
return content;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Creates a minimal valid CallToolResult with text content
|
|
253
|
+
*/
|
|
254
|
+
export function createSimpleResult(text, isError = false) {
|
|
255
|
+
return {
|
|
256
|
+
content: [createTextContent(text)],
|
|
257
|
+
...(isError && { isError: true }),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Result Types and Validators
|
|
3
|
+
*
|
|
4
|
+
* Provides type definitions and validation utilities for MCP CallToolResult
|
|
5
|
+
* schema compliance. Supports multiple content types, structured data,
|
|
6
|
+
* error handling, and metadata.
|
|
7
|
+
*/
|
|
8
|
+
import type { CallToolResult, ContentBlock, TextContent, ImageContent, AudioContent, ResourceLink, EmbeddedResource } from "@modelcontextprotocol/sdk/spec.types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Union type for tool execution results
|
|
11
|
+
* Tools can return:
|
|
12
|
+
* - string: Simple text result (backward compatible)
|
|
13
|
+
* - CallToolResult: Full MCP result with content, structured data, errors, metadata
|
|
14
|
+
* - object: Plain object that will be converted to structuredContent
|
|
15
|
+
*/
|
|
16
|
+
export type ToolExecuteResult = string | CallToolResult | Record<string, unknown> | null | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Type guards for result type detection
|
|
19
|
+
*/
|
|
20
|
+
export declare function isString(value: unknown): value is string;
|
|
21
|
+
/**
|
|
22
|
+
* Check if value looks like it's intended to be a CallToolResult
|
|
23
|
+
* (has content array field, regardless of validity)
|
|
24
|
+
*/
|
|
25
|
+
export declare function looksLikeCallToolResult(value: unknown): boolean;
|
|
26
|
+
export declare function isCallToolResult(value: unknown): value is CallToolResult;
|
|
27
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
28
|
+
export declare function isErrorResult(value: unknown): value is CallToolResult & {
|
|
29
|
+
isError: true;
|
|
30
|
+
};
|
|
31
|
+
export declare function isNullOrUndefined(value: unknown): value is null | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Type-safe ContentBlock validators
|
|
34
|
+
*/
|
|
35
|
+
export declare function isTextContent(block: unknown): block is TextContent;
|
|
36
|
+
export declare function isImageContent(block: unknown): block is ImageContent;
|
|
37
|
+
export declare function isAudioContent(block: unknown): block is AudioContent;
|
|
38
|
+
export declare function isResourceLink(block: unknown): block is ResourceLink;
|
|
39
|
+
export declare function isEmbeddedResource(block: unknown): block is EmbeddedResource;
|
|
40
|
+
export declare function isContentBlock(block: unknown): block is ContentBlock;
|
|
41
|
+
/**
|
|
42
|
+
* Validation result type
|
|
43
|
+
*/
|
|
44
|
+
export interface ValidationError {
|
|
45
|
+
field: string;
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ValidationResult {
|
|
49
|
+
valid: boolean;
|
|
50
|
+
errors: ValidationError[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validates a single ContentBlock against MCP schema
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateContentBlock(block: unknown): ValidationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Validates a complete CallToolResult against MCP schema
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateCallToolResult(result: unknown): ValidationResult;
|
|
60
|
+
/**
|
|
61
|
+
* Creates a TextContent block
|
|
62
|
+
*/
|
|
63
|
+
export declare function createTextContent(text: string, annotations?: any, _meta?: any): TextContent;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a minimal valid CallToolResult with text content
|
|
66
|
+
*/
|
|
67
|
+
export declare function createSimpleResult(text: string, isError?: boolean): CallToolResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Result Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Converts various tool execution result types into valid MCP CallToolResult
|
|
5
|
+
* format. Implements smart detection with strict validation and backward
|
|
6
|
+
* compatibility for string-returning tools.
|
|
7
|
+
*
|
|
8
|
+
* Supported Input Types:
|
|
9
|
+
* - string: Wrapped as TextContent
|
|
10
|
+
* - CallToolResult: Validated and returned as-is
|
|
11
|
+
* - Plain object: Converted to structuredContent with text summary
|
|
12
|
+
* - null/undefined: Returns empty content array
|
|
13
|
+
* - Error: Returns error CallToolResult
|
|
14
|
+
*/
|
|
15
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/spec.types.js";
|
|
16
|
+
import { type ToolExecuteResult } from "../types/mcp-result-types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Normalizes a tool execution result into a valid CallToolResult
|
|
19
|
+
*
|
|
20
|
+
* @param result - The result from tool.execute()
|
|
21
|
+
* @returns Valid CallToolResult with appropriate content and metadata
|
|
22
|
+
* @throws Error if result validation fails (strict mode)
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeToolResult(result: ToolExecuteResult): CallToolResult;
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a tool execution result with error handling
|
|
27
|
+
*
|
|
28
|
+
* Returns error in CallToolResult format instead of throwing
|
|
29
|
+
* Useful for handlers that want to catch and format errors
|
|
30
|
+
*
|
|
31
|
+
* @param result - The result from tool.execute()
|
|
32
|
+
* @returns Valid CallToolResult (never throws)
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeToolResultSafe(result: ToolExecuteResult): CallToolResult;
|
|
35
|
+
/**
|
|
36
|
+
* Normalizes an error into a CallToolResult with isError flag
|
|
37
|
+
*
|
|
38
|
+
* @param error - The error to normalize
|
|
39
|
+
* @returns CallToolResult with isError: true
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeToolError(error: unknown): CallToolResult;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Result Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Converts various tool execution result types into valid MCP CallToolResult
|
|
5
|
+
* format. Implements smart detection with strict validation and backward
|
|
6
|
+
* compatibility for string-returning tools.
|
|
7
|
+
*
|
|
8
|
+
* Supported Input Types:
|
|
9
|
+
* - string: Wrapped as TextContent
|
|
10
|
+
* - CallToolResult: Validated and returned as-is
|
|
11
|
+
* - Plain object: Converted to structuredContent with text summary
|
|
12
|
+
* - null/undefined: Returns empty content array
|
|
13
|
+
* - Error: Returns error CallToolResult
|
|
14
|
+
*/
|
|
15
|
+
import { isString, looksLikeCallToolResult, isPlainObject, isNullOrUndefined, validateCallToolResult, createTextContent, createSimpleResult, } from "../types/mcp-result-types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Generates a human-readable JSON summary of an object
|
|
18
|
+
* Limits depth and size for reasonable output
|
|
19
|
+
*/
|
|
20
|
+
function generateJsonSummary(obj) {
|
|
21
|
+
try {
|
|
22
|
+
// Limit JSON string length to 2000 chars
|
|
23
|
+
const json = JSON.stringify(obj, null, 2);
|
|
24
|
+
if (json.length > 2000) {
|
|
25
|
+
return json.substring(0, 2000) + "\n... (truncated)";
|
|
26
|
+
}
|
|
27
|
+
return json;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// If JSON serialization fails, try string representation
|
|
31
|
+
try {
|
|
32
|
+
const str = String(obj);
|
|
33
|
+
return str.length > 500 ? str.substring(0, 500) + "..." : str;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "[Unable to serialize object]";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes a tool execution result into a valid CallToolResult
|
|
42
|
+
*
|
|
43
|
+
* @param result - The result from tool.execute()
|
|
44
|
+
* @returns Valid CallToolResult with appropriate content and metadata
|
|
45
|
+
* @throws Error if result validation fails (strict mode)
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeToolResult(result) {
|
|
48
|
+
// Handle string results (backward compatibility)
|
|
49
|
+
if (isString(result)) {
|
|
50
|
+
return createSimpleResult(result);
|
|
51
|
+
}
|
|
52
|
+
// Handle null/undefined
|
|
53
|
+
if (isNullOrUndefined(result)) {
|
|
54
|
+
return {
|
|
55
|
+
content: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Check if it looks like CallToolResult (has content array)
|
|
59
|
+
// If so, validate strictly regardless of other fields
|
|
60
|
+
if (looksLikeCallToolResult(result)) {
|
|
61
|
+
const validation = validateCallToolResult(result);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
const errorMessages = validation.errors
|
|
64
|
+
.map((e) => `${e.field}: ${e.message}`)
|
|
65
|
+
.join("; ");
|
|
66
|
+
throw new Error(`Invalid CallToolResult: ${errorMessages}`);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
// Handle plain objects - convert to structuredContent with summary
|
|
71
|
+
if (isPlainObject(result)) {
|
|
72
|
+
const summary = generateJsonSummary(result);
|
|
73
|
+
return {
|
|
74
|
+
content: [createTextContent(`Result:\n\`\`\`json\n${summary}\n\`\`\``)],
|
|
75
|
+
structuredContent: result,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Shouldn't reach here, but handle unexpected types
|
|
79
|
+
return createSimpleResult(`[Unexpected result type: ${typeof result}]`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalizes a tool execution result with error handling
|
|
83
|
+
*
|
|
84
|
+
* Returns error in CallToolResult format instead of throwing
|
|
85
|
+
* Useful for handlers that want to catch and format errors
|
|
86
|
+
*
|
|
87
|
+
* @param result - The result from tool.execute()
|
|
88
|
+
* @returns Valid CallToolResult (never throws)
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeToolResultSafe(result) {
|
|
91
|
+
try {
|
|
92
|
+
return normalizeToolResult(result);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
return createSimpleResult(message, true);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Normalizes an error into a CallToolResult with isError flag
|
|
101
|
+
*
|
|
102
|
+
* @param error - The error to normalize
|
|
103
|
+
* @returns CallToolResult with isError: true
|
|
104
|
+
*/
|
|
105
|
+
export function normalizeToolError(error) {
|
|
106
|
+
let message;
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
message = error.message;
|
|
109
|
+
}
|
|
110
|
+
else if (typeof error === "string") {
|
|
111
|
+
message = error;
|
|
112
|
+
}
|
|
113
|
+
else if (typeof error === "object" && error !== null) {
|
|
114
|
+
message = JSON.stringify(error);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
message = String(error);
|
|
118
|
+
}
|
|
119
|
+
return createSimpleResult(message, true);
|
|
120
|
+
}
|
package/package.json
CHANGED