fix: return method-specific MCP responses (#18671)
## Summary Fixes #18524 Fixes the MCP response contract for non-`initialize` methods. Previously, `/mcp` returned initialize-style metadata for methods like `tools/list`, which caused strict MCP clients to reject the response shape. The endpoint also returned `201 Created` for RPC calls even though no resource was being created. ## Changes - return only method-specific payloads for MCP list methods - `tools/list` -> `{ tools: [...] }` - `prompts/list` -> `{ prompts: [] }` - `resources/list` -> `{ resources: [] }` - keep MCP server metadata only on `initialize` - make `/mcp` return `200 OK` instead of `201 Created` - add regression tests for: - `tools/list` response shape - `prompts/list` response shape - `resources/list` response shape ## Why Strict MCP clients expect: - standard RPC transport semantics over HTTP - method-specific JSON-RPC result payloads Returning initialize metadata for non-`initialize` methods breaks that expectation and can cause client deserialization or protocol validation failures. ## Verification - reproduced the issue locally against `/mcp` - verified `tools/list` was previously returning initialize-style fields - verified `tools/list` now returns only `result.tools` - verified `/mcp` now returns `200 OK` - ran targeted Jest tests: ```bash cd /Users/apple/MyProjects/OpenSource/twenty/packages/twenty-server npx jest --runInBand src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts src/engine/api/mcp/services/__tests__/mcp-tool-executor.service.spec.ts --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
6e36ad9fa2
commit
a07337fea0
+130
-20
@@ -1,7 +1,10 @@
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MCP_SERVER_METADATA } from 'src/engine/api/mcp/constants/mcp.const';
|
||||
import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const';
|
||||
import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const';
|
||||
import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const';
|
||||
import { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
|
||||
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
|
||||
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
@@ -97,21 +100,23 @@ describe('McpProtocolService', () => {
|
||||
});
|
||||
|
||||
describe('handleInitialize', () => {
|
||||
it('should return correct initialization response', () => {
|
||||
it('should return spec-compliant initialization response', () => {
|
||||
const requestId = '123';
|
||||
const result = service.handleInitialize(requestId);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
id: requestId,
|
||||
jsonrpc: '2.0',
|
||||
result: expect.objectContaining({
|
||||
...MCP_SERVER_METADATA,
|
||||
result: {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
resources: { listChanged: false },
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
}),
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -176,20 +181,37 @@ describe('McpProtocolService', () => {
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: expect.objectContaining({
|
||||
...MCP_SERVER_METADATA,
|
||||
result: {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
resources: { listChanged: false },
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
}),
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for notifications (no id)', async () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
};
|
||||
|
||||
const result = await service.handleMCPCoreQuery(mockRequest, {
|
||||
workspace: mockWorkspace,
|
||||
userWorkspaceId: mockUserWorkspaceId,
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should build a ToolSet with exactly 5 tools and pass it to executor for tools/call', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
@@ -197,7 +219,6 @@ describe('McpProtocolService', () => {
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
...MCP_SERVER_METADATA,
|
||||
content: [{ type: 'text', text: '{}' }],
|
||||
isError: false,
|
||||
},
|
||||
@@ -276,6 +297,71 @@ describe('McpProtocolService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return prompts list without role resolution', async () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'prompts/list',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const result = await service.handleMCPCoreQuery(mockRequest, {
|
||||
workspace: mockWorkspace,
|
||||
userWorkspaceId: mockUserWorkspaceId,
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: { prompts: [] },
|
||||
});
|
||||
expect(userRoleService.getRoleIdForUserWorkspace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return resources list without role resolution', async () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'resources/list',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const result = await service.handleMCPCoreQuery(mockRequest, {
|
||||
workspace: mockWorkspace,
|
||||
userWorkspaceId: mockUserWorkspaceId,
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: { resources: [] },
|
||||
});
|
||||
expect(userRoleService.getRoleIdForUserWorkspace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return method not found for unknown methods', async () => {
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'unknown/method',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const result = await service.handleMCPCoreQuery(mockRequest, {
|
||||
workspace: mockWorkspace,
|
||||
userWorkspaceId: mockUserWorkspaceId,
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.METHOD_NOT_FOUND,
|
||||
message: "Method 'unknown/method' not found",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tools/call with apiKey authentication', async () => {
|
||||
const mockToolCallResponse = {
|
||||
id: '123',
|
||||
@@ -306,20 +392,17 @@ describe('McpProtocolService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when tool execution fails', async () => {
|
||||
it('should wrap unexpected errors with INTERNAL_ERROR code', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
|
||||
|
||||
mcpToolExecutorService.handleToolCall.mockRejectedValue(
|
||||
new HttpException(
|
||||
"Tool 'nonExistentTool' not found",
|
||||
HttpStatus.NOT_FOUND,
|
||||
),
|
||||
new Error('Something went wrong'),
|
||||
);
|
||||
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: { name: 'nonExistentTool', arguments: {} },
|
||||
params: { name: 'execute_tool', arguments: {} },
|
||||
id: '123',
|
||||
};
|
||||
|
||||
@@ -333,9 +416,36 @@ describe('McpProtocolService', () => {
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
...MCP_SERVER_METADATA,
|
||||
code: HttpStatus.NOT_FOUND,
|
||||
message: "Tool 'nonExistentTool' not found",
|
||||
code: JSON_RPC_ERROR_CODE.INTERNAL_ERROR,
|
||||
message: 'Something went wrong',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap HttpException errors with SERVER_ERROR code', async () => {
|
||||
userRoleService.getRoleIdForUserWorkspace.mockRejectedValue(
|
||||
new HttpException('Role ID missing', HttpStatus.FORBIDDEN),
|
||||
);
|
||||
|
||||
const mockRequest: JsonRpc = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: { name: 'execute_tool', arguments: {} },
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const result = await service.handleMCPCoreQuery(mockRequest, {
|
||||
workspace: mockWorkspace,
|
||||
userWorkspaceId: mockUserWorkspaceId,
|
||||
apiKey: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.SERVER_ERROR,
|
||||
message: 'Role ID missing',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const';
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
|
||||
describe('McpToolExecutorService', () => {
|
||||
let service: McpToolExecutorService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new McpToolExecutorService();
|
||||
});
|
||||
|
||||
describe('handleToolsListing', () => {
|
||||
it('should return only tools array in result', () => {
|
||||
const toolSet = {
|
||||
test_tool: {
|
||||
description: 'A test tool',
|
||||
inputSchema: {
|
||||
jsonSchema: {
|
||||
type: 'object',
|
||||
properties: { query: { type: 'string' } },
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = service.handleToolsListing('123', toolSet);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
tools: [
|
||||
{
|
||||
name: 'test_tool',
|
||||
description: 'A test tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { query: { type: 'string' } },
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep inputSchema unchanged when it is already a plain schema', () => {
|
||||
const toolSet = {
|
||||
plain_tool: {
|
||||
description: 'A plain schema tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = service.handleToolsListing('456', toolSet);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '456',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
tools: [
|
||||
{
|
||||
name: 'plain_tool',
|
||||
description: 'A plain schema tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleToolCall', () => {
|
||||
it('should return JSON-RPC error with INVALID_PARAMS for unknown tools', async () => {
|
||||
const toolSet = {} as any;
|
||||
|
||||
const result = await service.handleToolCall('123', toolSet, {
|
||||
name: 'nonexistent_tool',
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.INVALID_PARAMS,
|
||||
message: 'Unknown tool: nonexistent_tool',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return result with isError: false on success', async () => {
|
||||
const toolSet = {
|
||||
my_tool: {
|
||||
execute: jest.fn().mockResolvedValue({ data: 'ok' }),
|
||||
description: 'My tool',
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await service.handleToolCall('123', toolSet, {
|
||||
name: 'my_tool',
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
content: [{ type: 'text', text: '{"data":"ok"}' }],
|
||||
isError: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return result with isError: true when tool execution throws', async () => {
|
||||
const toolSet = {
|
||||
failing_tool: {
|
||||
execute: jest.fn().mockRejectedValue(new Error('API rate limited')),
|
||||
description: 'A tool that fails',
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await service.handleToolCall('123', toolSet, {
|
||||
name: 'failing_tool',
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
content: [{ type: 'text', text: 'API rate limited' }],
|
||||
isError: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,10 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { type ToolSet, zodSchema } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const';
|
||||
import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const';
|
||||
import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const';
|
||||
import { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
|
||||
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
|
||||
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
|
||||
@@ -51,14 +55,14 @@ export class McpProtocolService {
|
||||
handleInitialize(requestId: string | number) {
|
||||
return wrapJsonRpcResponse(requestId, {
|
||||
result: {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
resources: { listChanged: false },
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
tools: [],
|
||||
resources: [],
|
||||
prompts: [],
|
||||
serverInfo: MCP_SERVER_INFO,
|
||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -152,6 +156,7 @@ export class McpProtocolService {
|
||||
};
|
||||
}
|
||||
|
||||
// Returns null for JSON-RPC notifications (no id), which require no response body
|
||||
async handleMCPCoreQuery(
|
||||
{ id, method, params }: JsonRpc,
|
||||
{
|
||||
@@ -165,20 +170,40 @@ export class McpProtocolService {
|
||||
userWorkspaceId?: string;
|
||||
apiKey: ApiKeyEntity | undefined;
|
||||
},
|
||||
): Promise<Record<string, unknown>> {
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
// JSON-RPC notifications have no id and expect no response
|
||||
if (!isDefined(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method === 'initialize') {
|
||||
return this.handleInitialize(id);
|
||||
}
|
||||
|
||||
if (method === 'ping') {
|
||||
return wrapJsonRpcResponse(
|
||||
id,
|
||||
{
|
||||
result: {},
|
||||
return wrapJsonRpcResponse(id, { result: {} });
|
||||
}
|
||||
|
||||
if (method === 'prompts/list') {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: { prompts: [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'resources/list') {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: { resources: [] },
|
||||
});
|
||||
}
|
||||
|
||||
if (method !== 'tools/list' && method !== 'tools/call') {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.METHOD_NOT_FOUND,
|
||||
message: `Method '${method}' not found`,
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const roleId = await this.getRoleId(
|
||||
@@ -197,7 +222,16 @@ export class McpProtocolService {
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
if (method === 'tools/call' && params) {
|
||||
if (method === 'tools/call') {
|
||||
if (!params) {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.INVALID_PARAMS,
|
||||
message: 'tools/call requires params with name and arguments',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await this.mcpToolExecutorService.handleToolCall(
|
||||
id,
|
||||
toolSet,
|
||||
@@ -205,40 +239,22 @@ export class McpProtocolService {
|
||||
);
|
||||
}
|
||||
|
||||
if (method === 'tools/list') {
|
||||
return this.mcpToolExecutorService.handleToolsListing(id, toolSet);
|
||||
}
|
||||
|
||||
if (method === 'prompts/list') {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {
|
||||
capabilities: {
|
||||
prompts: { listChanged: false },
|
||||
},
|
||||
prompts: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'resources/list') {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {
|
||||
capabilities: {
|
||||
resources: { listChanged: false },
|
||||
},
|
||||
resources: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {},
|
||||
});
|
||||
return this.mcpToolExecutorService.handleToolsListing(id, toolSet);
|
||||
} catch (error) {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
if (error instanceof HttpException) {
|
||||
return wrapJsonRpcResponse(id ?? 0, {
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.SERVER_ERROR,
|
||||
message: error.message || 'Request failed',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return wrapJsonRpcResponse(id ?? 0, {
|
||||
error: {
|
||||
code: error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
message: error.message || 'Failed to execute tool',
|
||||
code: JSON_RPC_ERROR_CODE.INTERNAL_ERROR,
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Internal server error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { JSON_RPC_ERROR_CODE } from 'src/engine/api/mcp/constants/json-rpc-error-code.const';
|
||||
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -15,29 +16,43 @@ export class McpToolExecutorService {
|
||||
const toolName = params.name as keyof typeof toolSet;
|
||||
const tool = toolSet[toolName];
|
||||
|
||||
if (isDefined(tool) && isDefined(tool.execute)) {
|
||||
if (!isDefined(tool) || !isDefined(tool.execute)) {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
error: {
|
||||
code: JSON_RPC_ERROR_CODE.INVALID_PARAMS,
|
||||
message: `Unknown tool: ${String(params.name)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await tool.execute(params.arguments, {
|
||||
toolCallId: '1',
|
||||
messages: [],
|
||||
});
|
||||
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {
|
||||
content: [{ type: 'text', text: JSON.stringify(result) }],
|
||||
isError: false,
|
||||
},
|
||||
});
|
||||
} catch (executionError) {
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
await tool.execute(params.arguments, {
|
||||
toolCallId: '1',
|
||||
messages: [],
|
||||
}),
|
||||
),
|
||||
text:
|
||||
executionError instanceof Error
|
||||
? executionError.message
|
||||
: 'Tool execution failed',
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
isError: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
`Tool '${params.name}' not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
handleToolsListing(id: string | number, toolSet: ToolSet) {
|
||||
@@ -63,12 +78,7 @@ export class McpToolExecutorService {
|
||||
|
||||
return wrapJsonRpcResponse(id, {
|
||||
result: {
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
},
|
||||
tools: toolsArray,
|
||||
resources: [],
|
||||
prompts: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user