diff --git a/packages/agent-toolkit/src/mcp/toolkit.test.ts b/packages/agent-toolkit/src/mcp/toolkit.test.ts index 31e053ad..2241eb3d 100644 --- a/packages/agent-toolkit/src/mcp/toolkit.test.ts +++ b/packages/agent-toolkit/src/mcp/toolkit.test.ts @@ -1008,6 +1008,99 @@ describe('MondayAgentToolkit', () => { }); }); + it('should include tool metadata in MCP-formatted results', async () => { + const complexity = { + query: 12500, + before: 9950000, + after: 9937500, + reset_in_x_seconds: 42, + }; + const mockTool = { + name: 'mcp-metadata-tool', + type: ToolType.READ, + annotations: { audience: [] }, + enabledByDefault: true, + getDescription: jest.fn().mockReturnValue('MCP metadata tool'), + getInputSchema: jest.fn().mockReturnValue({ input: z.string() }), + execute: jest.fn().mockResolvedValue({ + content: { message: 'Test content' }, + metadata: { complexity }, + }), + }; + + mockGetFilteredToolInstances.mockReturnValue([mockTool]); + + const toolkit = new MondayAgentToolkit({ + mondayApiToken: 'test-token', + }); + + const [tool] = toolkit.getToolsForMcp(); + const result = await tool.handler({ input: 'test' }); + + expect(result).toEqual({ + structuredContent: { message: 'Test content' }, + content: [{ type: 'text', text: JSON.stringify({ message: 'Test content' }) }], + _meta: { complexity }, + }); + }); + + it('should expose monday API complexity metadata captured during MCP tool calls', async () => { + const complexity = { + query: 12500, + before: 9950000, + after: 9937500, + reset_in_x_seconds: 42, + }; + const mockRequest = jest.fn().mockResolvedValue({ + complexity, + boards: [{ id: '123' }], + }); + + mockApiClient.mockImplementationOnce(() => ({ request: mockRequest }) as any); + mockGetFilteredToolInstances.mockImplementationOnce(({ apiClient }) => [ + { + name: 'api-complexity-tool', + type: ToolType.READ, + annotations: { audience: [] }, + enabledByDefault: true, + getDescription: jest.fn().mockReturnValue('API complexity tool'), + getInputSchema: jest.fn().mockReturnValue({ boardId: z.string() }), + execute: jest.fn().mockImplementation(async ({ boardId }) => { + const response = await apiClient.request( + `query getBoard($boardId: [ID!]) { + boards(ids: $boardId) { + id + } + }`, + { boardId }, + ); + + return { content: response }; + }), + }, + ]); + + const toolkit = new MondayAgentToolkit({ + mondayApiToken: 'test-token', + }); + + const [tool] = toolkit.getToolsForMcp(); + const result = await tool.handler({ boardId: '123' }); + + expect(mockRequest).toHaveBeenCalledWith( + expect.stringContaining( + 'complexity {\n query\n before\n after\n reset_in_x_seconds\n }', + ), + { boardId: '123' }, + undefined, + ); + expect(result).toEqual({ + structuredContent: { boards: [{ id: '123' }] }, + content: [{ type: 'text', text: JSON.stringify({ boards: [{ id: '123' }] }) }], + _meta: { complexity }, + }); + }); + it('should handle errors in MCP format', async () => { const mockTool = { name: 'mcp-error-tool', diff --git a/packages/agent-toolkit/src/mcp/toolkit.ts b/packages/agent-toolkit/src/mcp/toolkit.ts index 83e9efcb..824020a6 100644 --- a/packages/agent-toolkit/src/mcp/toolkit.ts +++ b/packages/agent-toolkit/src/mcp/toolkit.ts @@ -1,6 +1,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { CallToolResult, ServerCapabilities } from '@modelcontextprotocol/sdk/types'; -import { ApiClient } from '@mondaydotcomorg/api'; +import { ApiClient, QueryVariables, RequestOptions } from '@mondaydotcomorg/api'; import { getFilteredToolInstances } from '../utils/tools/tools-filtering.utils'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; @@ -15,11 +15,35 @@ export interface GetToolsOptions { schemaFormat?: 'zod' | 'json'; } +type ApiComplexity = { + query?: number | null; + before?: number | null; + after?: number | null; + reset_in_x_seconds?: number | null; +}; + +type ComplexityMetadata = { + complexity: ApiComplexity; + complexities?: ApiComplexity[]; +}; + +type ComplexityTrackingApiClient = ApiClient & { + startComplexityTracking?: () => void; + consumeComplexityMetadata?: () => ComplexityMetadata | undefined; +}; + +const COMPLEXITY_SELECTION = `complexity { + query + before + after + reset_in_x_seconds + }`; + /** * Monday Agent Toolkit providing an MCP server with monday.com tools */ export class MondayAgentToolkit extends McpServer { - private readonly mondayApiClient: ApiClient; + private readonly mondayApiClient: ComplexityTrackingApiClient; private readonly mondayApiToken: string; private readonly context?: MondayAgentToolkitConfig['context']; private readonly dynamicToolManager: DynamicToolManager = new DynamicToolManager(); @@ -59,7 +83,7 @@ export class MondayAgentToolkit extends McpServer { * Create and configure the Monday API client */ private createApiClient(config: MondayAgentToolkitConfig): ApiClient { - return new ApiClient({ + const client = new ApiClient({ token: config.mondayApiToken, apiVersion: config.mondayApiVersion ?? API_VERSION, endpoint: config.mondayApiEndpoint, @@ -71,6 +95,109 @@ export class MondayAgentToolkit extends McpServer { }, }, }); + + return this.createComplexityTrackingClient(client); + } + + private createComplexityTrackingClient(client: ApiClient): ComplexityTrackingApiClient { + const trackedClient = client as ComplexityTrackingApiClient; + if (typeof client.request !== 'function') { + return trackedClient; + } + + const originalRequest = client.request.bind(client); + let isTracking = false; + let complexities: ApiComplexity[] = []; + + trackedClient.startComplexityTracking = () => { + isTracking = true; + complexities = []; + }; + + trackedClient.consumeComplexityMetadata = () => { + isTracking = false; + const trackedComplexities = complexities; + complexities = []; + + if (!trackedComplexities.length) { + return undefined; + } + + const latestComplexity = trackedComplexities[trackedComplexities.length - 1]; + + return { + complexity: latestComplexity, + ...(trackedComplexities.length > 1 ? { complexities: trackedComplexities } : {}), + }; + }; + + trackedClient.request = async ( + query: string, + variables?: QueryVariables, + options?: RequestOptions, + ): Promise => { + const complexityQuery = isTracking ? this.addComplexitySelection(query) : { query, injected: false }; + const response = await originalRequest(complexityQuery.query, variables, options); + const complexity = this.extractComplexity(response); + + if (isTracking && complexity) { + complexities.push(complexity); + } + + if (complexityQuery.injected && this.isRecord(response)) { + const { complexity: _complexity, ...responseWithoutComplexity } = response; + return responseWithoutComplexity as T; + } + + return response; + }; + + return trackedClient; + } + + private addComplexitySelection(query: string): { query: string; injected: boolean } { + if (!query || /\bcomplexity\s*\{/i.test(query)) { + return { query, injected: false }; + } + + const operationStart = query.search(/\b(query|mutation)\b/i); + if (operationStart === -1) { + return { query, injected: false }; + } + + const selectionStart = query.indexOf('{', operationStart); + if (selectionStart === -1) { + return { query, injected: false }; + } + + return { + query: `${query.slice(0, selectionStart + 1)} + ${COMPLEXITY_SELECTION}${query.slice(selectionStart + 1)}`, + injected: true, + }; + } + + private extractComplexity(response: unknown): ApiComplexity | undefined { + if (!this.isRecord(response) || !this.isRecord(response.complexity)) { + return undefined; + } + + const complexity = response.complexity; + + return { + query: this.getNullableNumber(complexity.query), + before: this.getNullableNumber(complexity.before), + after: this.getNullableNumber(complexity.after), + reset_in_x_seconds: this.getNullableNumber(complexity.reset_in_x_seconds), + }; + } + + private getNullableNumber(value: unknown): number | null | undefined { + return typeof value === 'number' || value === null ? value : undefined; + } + + private isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value); } /** @@ -133,17 +260,8 @@ export class MondayAgentToolkit extends McpServer { }, async (args: any, _extra: any) => { try { - let result; - if (inputSchema) { - const parsedArgs = z.object(inputSchema).safeParse(args); - if (!parsedArgs.success) { - throw new Error(`Invalid arguments: ${parsedArgs.error.message}`); - } - result = await tool.execute(parsedArgs.data); - } else { - result = await tool.execute(); - } - return this.formatToolResult(result.content); + const result = await this.executeTool(tool, args); + return this.formatToolResult(result.content, result.metadata); } catch (error) { return this.handleToolError(error, tool.name); } @@ -283,20 +401,8 @@ export class MondayAgentToolkit extends McpServer { private createMcpToolHandler(tool: Tool) { return async (params: any, extra?: any): Promise => { try { - const inputSchema = tool.getInputSchema(); - - if (inputSchema) { - // inputSchema is already a Zod schema object definition, so we wrap it with z.object() - const parsedArgs = z.object(inputSchema).safeParse(params); - if (!parsedArgs.success) { - throw new Error(`Invalid arguments: ${parsedArgs.error.message}`); - } - const result = await tool.execute(parsedArgs.data, extra); - return this.formatToolResult(result.content); - } else { - const result = await tool.execute(undefined, extra); - return this.formatToolResult(result.content); - } + const result = await this.executeTool(tool, params, extra); + return this.formatToolResult(result.content, result.metadata); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { @@ -321,26 +427,75 @@ export class MondayAgentToolkit extends McpServer { } if (options?.schemaFormat === 'json') { - return zodToJsonSchema(z.object(inputSchema)); + return zodToJsonSchema(z.object(inputSchema) as any); } return inputSchema; } + /** + * Execute a tool and attach any API complexity data captured during the call. + */ + private async executeTool(tool: Tool, args?: any, extra?: any) { + const inputSchema = tool.getInputSchema(); + const tracker = this.mondayApiClient; + + tracker.startComplexityTracking?.(); + + try { + const result = inputSchema + ? await tool.execute(this.parseToolArgs(inputSchema, args), extra) + : await tool.execute(undefined, extra); + const complexityMetadata = tracker.consumeComplexityMetadata?.(); + + if (!complexityMetadata) { + return result; + } + + return { + ...result, + metadata: { + ...result.metadata, + ...complexityMetadata, + }, + }; + } catch (error) { + tracker.consumeComplexityMetadata?.(); + throw error; + } + } + + private parseToolArgs(inputSchema: z.ZodRawShape, args: any) { + const parsedArgs = z.object(inputSchema).safeParse(args); + if (!parsedArgs.success) { + throw new Error(`Invalid arguments: ${parsedArgs.error.message}`); + } + + return parsedArgs.data; + } + /** * Format the tool result into the expected MCP format */ - private formatToolResult(content: string | Record): CallToolResult { - if(typeof content === 'string') { + private formatToolResult(content: string | Record, metadata?: Record): CallToolResult { + const result: CallToolResult = + typeof content === 'string' + ? { + content: [{ type: 'text', text: content }], + } + : { + structuredContent: content, + content: [{ type: 'text', text: JSON.stringify(content) }], + }; + + if (metadata && Object.keys(metadata).length > 0) { return { - content: [{ type: 'text', text: content }], - } + ...result, + _meta: metadata, + }; } - - return { - structuredContent: content, - content: [{ type: 'text', text: JSON.stringify(content) }] - }; + + return result; } /**