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.
@@ -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: `${url.pathname} endpoint not implemented` }, 501);
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: "2025-11-25",
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
- const { ERROR_METHOD_NOT_FOUND } = await import("modality-kit");
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.3",
2
+ "version": "1.2.0",
3
3
  "name": "modality-mcp-kit",
4
4
  "repository": {
5
5
  "type": "git",