From e1f7c478509b78758631eb3b5add5d4f2587a81a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 17:12:10 -0400 Subject: [PATCH] Do another refactoring pass on `src/mcp` Signed-off-by: Juan Cruz Viotti --- .../e2e/html/hurl/mcp-2025-03-26.all.hurl | 44 +++ .../e2e/html/hurl/mcp-2025-06-18.all.hurl | 4 + .../html/hurl/mcp-2025-11-25-gzip.all.hurl | 16 + .../hurl/mcp-2025-11-25-resources.all.hurl | 4 +- .../html/hurl/mcp-2025-11-25-tools.all.hurl | 44 +++ .../e2e/path/hurl/mcp-2025-03-26.all.hurl | 44 +++ .../e2e/path/hurl/mcp-2025-06-18.all.hurl | 4 + .../path/hurl/mcp-2025-11-25-gzip.all.hurl | 16 + .../hurl/mcp-2025-11-25-resources.all.hurl | 4 +- .../path/hurl/mcp-2025-11-25-tools.all.hurl | 44 +++ enterprise/index/enterprise_index.cc | 26 +- enterprise/server/action_mcp_v1.cc | 92 ++++-- .../one/enterprise_server_actions.h | 4 + src/actions/action_default_v1.h | 4 + src/actions/action_dependency_tree_v1.h | 4 + src/actions/action_health_check_v1.h | 4 + src/actions/action_jsonschema_evaluate_v1.h | 4 + src/actions/action_jsonschema_trace_v1.h | 4 + src/actions/action_list_directory_v1.h | 4 + src/actions/action_mcp_v1.h | 22 +- src/actions/action_not_found_v1.h | 4 + src/actions/action_schema_search_v1.h | 4 + .../action_serve_explorer_artifact_v1.h | 4 + src/actions/action_serve_schema_artifact_v1.h | 4 + src/actions/action_serve_static_v1.h | 4 + src/actions/actions.cc | 63 +++- src/actions/include/sourcemeta/one/actions.h | 17 +- src/index/explorer.h | 43 ++- src/mcp/include/sourcemeta/one/mcp.h | 93 ++++-- src/mcp/mcp.cc | 312 +++++++++++++----- src/router/include/sourcemeta/one/router.h | 8 +- src/router/router.cc | 12 +- src/self/v1/schemas/mcp/response.json | 6 +- .../v1/schemas/mcp/tools/list/response.json | 26 +- src/server/server.cc | 4 +- 35 files changed, 788 insertions(+), 208 deletions(-) diff --git a/enterprise/e2e/html/hurl/mcp-2025-03-26.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-03-26.all.hurl index e34980b1..aa2cba5c 100644 --- a/enterprise/e2e/html/hurl/mcp-2025-03-26.all.hurl +++ b/enterprise/e2e/html/hurl/mcp-2025-03-26.all.hurl @@ -176,28 +176,72 @@ jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].description" == "List the contents of a directory in the catalog" jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[1].name" == "get_schema_dependencies" jsonpath "$.result.tools[1].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[1].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/dependencies/rpc" jsonpath "$.result.tools[1].annotations.title" == "Get Schema Dependencies" +jsonpath "$.result.tools[1].annotations.readOnlyHint" == true +jsonpath "$.result.tools[1].annotations.destructiveHint" == false +jsonpath "$.result.tools[1].annotations.idempotentHint" == true +jsonpath "$.result.tools[1].annotations.openWorldHint" == false jsonpath "$.result.tools[2].name" == "get_schema_dependents" jsonpath "$.result.tools[2].annotations.title" == "Get Schema Dependents" +jsonpath "$.result.tools[2].annotations.readOnlyHint" == true +jsonpath "$.result.tools[2].annotations.destructiveHint" == false +jsonpath "$.result.tools[2].annotations.idempotentHint" == true +jsonpath "$.result.tools[2].annotations.openWorldHint" == false jsonpath "$.result.tools[3].name" == "get_schema_health" jsonpath "$.result.tools[3].annotations.title" == "Get Schema Health" +jsonpath "$.result.tools[3].annotations.readOnlyHint" == true +jsonpath "$.result.tools[3].annotations.destructiveHint" == false +jsonpath "$.result.tools[3].annotations.idempotentHint" == true +jsonpath "$.result.tools[3].annotations.openWorldHint" == false jsonpath "$.result.tools[4].name" == "get_schema_locations" jsonpath "$.result.tools[4].annotations.title" == "Get Schema Locations" +jsonpath "$.result.tools[4].annotations.readOnlyHint" == true +jsonpath "$.result.tools[4].annotations.destructiveHint" == false +jsonpath "$.result.tools[4].annotations.idempotentHint" == true +jsonpath "$.result.tools[4].annotations.openWorldHint" == false jsonpath "$.result.tools[5].name" == "get_schema_positions" jsonpath "$.result.tools[5].annotations.title" == "Get Schema Positions" +jsonpath "$.result.tools[5].annotations.readOnlyHint" == true +jsonpath "$.result.tools[5].annotations.destructiveHint" == false +jsonpath "$.result.tools[5].annotations.idempotentHint" == true +jsonpath "$.result.tools[5].annotations.openWorldHint" == false jsonpath "$.result.tools[6].name" == "get_schema_stats" jsonpath "$.result.tools[6].annotations.title" == "Get Schema Stats" +jsonpath "$.result.tools[6].annotations.readOnlyHint" == true +jsonpath "$.result.tools[6].annotations.destructiveHint" == false +jsonpath "$.result.tools[6].annotations.idempotentHint" == true +jsonpath "$.result.tools[6].annotations.openWorldHint" == false jsonpath "$.result.tools[7].name" == "get_schema_metadata" jsonpath "$.result.tools[7].annotations.title" == "Get Schema Metadata" +jsonpath "$.result.tools[7].annotations.readOnlyHint" == true +jsonpath "$.result.tools[7].annotations.destructiveHint" == false +jsonpath "$.result.tools[7].annotations.idempotentHint" == true +jsonpath "$.result.tools[7].annotations.openWorldHint" == false jsonpath "$.result.tools[8].name" == "evaluate_schema" jsonpath "$.result.tools[8].annotations.title" == "Evaluate Schema" +jsonpath "$.result.tools[8].annotations.readOnlyHint" == true +jsonpath "$.result.tools[8].annotations.destructiveHint" == false +jsonpath "$.result.tools[8].annotations.idempotentHint" == true +jsonpath "$.result.tools[8].annotations.openWorldHint" == false jsonpath "$.result.tools[9].name" == "trace_schema_evaluation" jsonpath "$.result.tools[9].annotations.title" == "Trace Schema Evaluation" +jsonpath "$.result.tools[9].annotations.readOnlyHint" == true +jsonpath "$.result.tools[9].annotations.destructiveHint" == false +jsonpath "$.result.tools[9].annotations.idempotentHint" == true +jsonpath "$.result.tools[9].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false # Added in 2025-06-18 ("outputSchema" on Tool) # https://modelcontextprotocol.io/specification/2025-06-18/changelog jsonpath "$..outputSchema" not exists diff --git a/enterprise/e2e/html/hurl/mcp-2025-06-18.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-06-18.all.hurl index 400308aa..7a96af69 100644 --- a/enterprise/e2e/html/hurl/mcp-2025-06-18.all.hurl +++ b/enterprise/e2e/html/hurl/mcp-2025-06-18.all.hurl @@ -167,6 +167,10 @@ jsonpath "$.result.tools[0].description" == "List the contents of a directory in jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/list/response" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false # Added in 2025-11-25 (SEP-973) # https://github.com/modelcontextprotocol/modelcontextprotocol/issues/973 jsonpath "$..icons" not exists diff --git a/enterprise/e2e/html/hurl/mcp-2025-11-25-gzip.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-11-25-gzip.all.hurl index 47840b9f..ef9b739b 100644 --- a/enterprise/e2e/html/hurl/mcp-2025-11-25-gzip.all.hurl +++ b/enterprise/e2e/html/hurl/mcp-2025-11-25-gzip.all.hurl @@ -160,8 +160,16 @@ jsonpath "$.id" == "tools-identity" jsonpath "$.result.tools" count == 11 jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} ``` @@ -192,8 +200,16 @@ jsonpath "$.id" == "tools-gzip" jsonpath "$.result.tools" count == 11 jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} ``` diff --git a/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl index 2dc8132d..8098f04e 100644 --- a/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl +++ b/enterprise/e2e/html/hurl/mcp-2025-11-25-resources.all.hurl @@ -223,7 +223,7 @@ jsonpath "$.result.resources[40].uri" == "{{base}}/self/v1/schemas/mcp/response" jsonpath "$.result.resources[40].name" == "response" jsonpath "$.result.resources[40].description" == "Any outgoing MCP response the server returns" jsonpath "$.result.resources[40].mimeType" == "application/schema+json" -jsonpath "$.result.resources[40].size" == 2965 +jsonpath "$.result.resources[40].size" == 3117 jsonpath "$.result.resources[41].uri" == "{{base}}/self/v1/schemas/mcp/tools/call/request" jsonpath "$.result.resources[41].name" == "request" jsonpath "$.result.resources[41].description" == "Request from a client to invoke an MCP tool by name" @@ -243,7 +243,7 @@ jsonpath "$.result.resources[44].uri" == "{{base}}/self/v1/schemas/mcp/tools/lis jsonpath "$.result.resources[44].name" == "response" jsonpath "$.result.resources[44].description" == "Sourcemeta One's list of MCP tools" jsonpath "$.result.resources[44].mimeType" == "application/schema+json" -jsonpath "$.result.resources[44].size" == 3106 +jsonpath "$.result.resources[44].size" == 3820 jsonpath "$.result.resources[45].uri" == "{{base}}/test/object" jsonpath "$.result.resources[45].name" == "object" jsonpath "$.result.resources[45].description" == "An Object Example" diff --git a/enterprise/e2e/html/hurl/mcp-2025-11-25-tools.all.hurl b/enterprise/e2e/html/hurl/mcp-2025-11-25-tools.all.hurl index 6fceb716..3dcc5224 100644 --- a/enterprise/e2e/html/hurl/mcp-2025-11-25-tools.all.hurl +++ b/enterprise/e2e/html/hurl/mcp-2025-11-25-tools.all.hurl @@ -24,56 +24,100 @@ jsonpath "$.result.tools[0].description" == "List the contents of a directory in jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/list/response" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[1].name" == "get_schema_dependencies" jsonpath "$.result.tools[1].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[1].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/dependencies/rpc" jsonpath "$.result.tools[1].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/dependencies/response" jsonpath "$.result.tools[1].annotations.title" == "Get Schema Dependencies" +jsonpath "$.result.tools[1].annotations.readOnlyHint" == true +jsonpath "$.result.tools[1].annotations.destructiveHint" == false +jsonpath "$.result.tools[1].annotations.idempotentHint" == true +jsonpath "$.result.tools[1].annotations.openWorldHint" == false jsonpath "$.result.tools[2].name" == "get_schema_dependents" jsonpath "$.result.tools[2].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[2].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/dependents/rpc" jsonpath "$.result.tools[2].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/dependents/response" jsonpath "$.result.tools[2].annotations.title" == "Get Schema Dependents" +jsonpath "$.result.tools[2].annotations.readOnlyHint" == true +jsonpath "$.result.tools[2].annotations.destructiveHint" == false +jsonpath "$.result.tools[2].annotations.idempotentHint" == true +jsonpath "$.result.tools[2].annotations.openWorldHint" == false jsonpath "$.result.tools[3].name" == "get_schema_health" jsonpath "$.result.tools[3].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[3].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/health/rpc" jsonpath "$.result.tools[3].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/health/response" jsonpath "$.result.tools[3].annotations.title" == "Get Schema Health" +jsonpath "$.result.tools[3].annotations.readOnlyHint" == true +jsonpath "$.result.tools[3].annotations.destructiveHint" == false +jsonpath "$.result.tools[3].annotations.idempotentHint" == true +jsonpath "$.result.tools[3].annotations.openWorldHint" == false jsonpath "$.result.tools[4].name" == "get_schema_locations" jsonpath "$.result.tools[4].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[4].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/locations/rpc" jsonpath "$.result.tools[4].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/locations/response" jsonpath "$.result.tools[4].annotations.title" == "Get Schema Locations" +jsonpath "$.result.tools[4].annotations.readOnlyHint" == true +jsonpath "$.result.tools[4].annotations.destructiveHint" == false +jsonpath "$.result.tools[4].annotations.idempotentHint" == true +jsonpath "$.result.tools[4].annotations.openWorldHint" == false jsonpath "$.result.tools[5].name" == "get_schema_positions" jsonpath "$.result.tools[5].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[5].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/positions/rpc" jsonpath "$.result.tools[5].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/positions/response" jsonpath "$.result.tools[5].annotations.title" == "Get Schema Positions" +jsonpath "$.result.tools[5].annotations.readOnlyHint" == true +jsonpath "$.result.tools[5].annotations.destructiveHint" == false +jsonpath "$.result.tools[5].annotations.idempotentHint" == true +jsonpath "$.result.tools[5].annotations.openWorldHint" == false jsonpath "$.result.tools[6].name" == "get_schema_stats" jsonpath "$.result.tools[6].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[6].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/stats/rpc" jsonpath "$.result.tools[6].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/stats/response" jsonpath "$.result.tools[6].annotations.title" == "Get Schema Stats" +jsonpath "$.result.tools[6].annotations.readOnlyHint" == true +jsonpath "$.result.tools[6].annotations.destructiveHint" == false +jsonpath "$.result.tools[6].annotations.idempotentHint" == true +jsonpath "$.result.tools[6].annotations.openWorldHint" == false jsonpath "$.result.tools[7].name" == "get_schema_metadata" jsonpath "$.result.tools[7].description" == "Read a navigation artifact for browsing schemas" jsonpath "$.result.tools[7].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/metadata/rpc" jsonpath "$.result.tools[7].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/metadata/response" jsonpath "$.result.tools[7].annotations.title" == "Get Schema Metadata" +jsonpath "$.result.tools[7].annotations.readOnlyHint" == true +jsonpath "$.result.tools[7].annotations.destructiveHint" == false +jsonpath "$.result.tools[7].annotations.idempotentHint" == true +jsonpath "$.result.tools[7].annotations.openWorldHint" == false jsonpath "$.result.tools[8].name" == "evaluate_schema" jsonpath "$.result.tools[8].description" == "Validate a JSON instance against a schema and return whether it is valid plus any errors" jsonpath "$.result.tools[8].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/evaluate/rpc" jsonpath "$.result.tools[8].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/evaluate/response" jsonpath "$.result.tools[8].annotations.title" == "Evaluate Schema" +jsonpath "$.result.tools[8].annotations.readOnlyHint" == true +jsonpath "$.result.tools[8].annotations.destructiveHint" == false +jsonpath "$.result.tools[8].annotations.idempotentHint" == true +jsonpath "$.result.tools[8].annotations.openWorldHint" == false jsonpath "$.result.tools[9].name" == "trace_schema_evaluation" jsonpath "$.result.tools[9].description" == "Validate a JSON instance against a schema and return detailed information about every step of the evaluation process" jsonpath "$.result.tools[9].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/trace/rpc" jsonpath "$.result.tools[9].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/trace/response" jsonpath "$.result.tools[9].annotations.title" == "Trace Schema Evaluation" +jsonpath "$.result.tools[9].annotations.readOnlyHint" == true +jsonpath "$.result.tools[9].annotations.destructiveHint" == false +jsonpath "$.result.tools[9].annotations.idempotentHint" == true +jsonpath "$.result.tools[9].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].description" == "Search for schemas by query term" jsonpath "$.result.tools[10].inputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/search/rpc" jsonpath "$.result.tools[10].outputSchema['$ref']" == "{{base}}/self/v1/schemas/api/schemas/search/response" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} ``` diff --git a/enterprise/e2e/path/hurl/mcp-2025-03-26.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-03-26.all.hurl index aa384f2c..958921c4 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-03-26.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-03-26.all.hurl @@ -176,28 +176,72 @@ jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].description" == "List the contents of a directory in the catalog" jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[1].name" == "get_schema_dependencies" jsonpath "$.result.tools[1].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[1].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/dependencies/rpc" jsonpath "$.result.tools[1].annotations.title" == "Get Schema Dependencies" +jsonpath "$.result.tools[1].annotations.readOnlyHint" == true +jsonpath "$.result.tools[1].annotations.destructiveHint" == false +jsonpath "$.result.tools[1].annotations.idempotentHint" == true +jsonpath "$.result.tools[1].annotations.openWorldHint" == false jsonpath "$.result.tools[2].name" == "get_schema_dependents" jsonpath "$.result.tools[2].annotations.title" == "Get Schema Dependents" +jsonpath "$.result.tools[2].annotations.readOnlyHint" == true +jsonpath "$.result.tools[2].annotations.destructiveHint" == false +jsonpath "$.result.tools[2].annotations.idempotentHint" == true +jsonpath "$.result.tools[2].annotations.openWorldHint" == false jsonpath "$.result.tools[3].name" == "get_schema_health" jsonpath "$.result.tools[3].annotations.title" == "Get Schema Health" +jsonpath "$.result.tools[3].annotations.readOnlyHint" == true +jsonpath "$.result.tools[3].annotations.destructiveHint" == false +jsonpath "$.result.tools[3].annotations.idempotentHint" == true +jsonpath "$.result.tools[3].annotations.openWorldHint" == false jsonpath "$.result.tools[4].name" == "get_schema_locations" jsonpath "$.result.tools[4].annotations.title" == "Get Schema Locations" +jsonpath "$.result.tools[4].annotations.readOnlyHint" == true +jsonpath "$.result.tools[4].annotations.destructiveHint" == false +jsonpath "$.result.tools[4].annotations.idempotentHint" == true +jsonpath "$.result.tools[4].annotations.openWorldHint" == false jsonpath "$.result.tools[5].name" == "get_schema_positions" jsonpath "$.result.tools[5].annotations.title" == "Get Schema Positions" +jsonpath "$.result.tools[5].annotations.readOnlyHint" == true +jsonpath "$.result.tools[5].annotations.destructiveHint" == false +jsonpath "$.result.tools[5].annotations.idempotentHint" == true +jsonpath "$.result.tools[5].annotations.openWorldHint" == false jsonpath "$.result.tools[6].name" == "get_schema_stats" jsonpath "$.result.tools[6].annotations.title" == "Get Schema Stats" +jsonpath "$.result.tools[6].annotations.readOnlyHint" == true +jsonpath "$.result.tools[6].annotations.destructiveHint" == false +jsonpath "$.result.tools[6].annotations.idempotentHint" == true +jsonpath "$.result.tools[6].annotations.openWorldHint" == false jsonpath "$.result.tools[7].name" == "get_schema_metadata" jsonpath "$.result.tools[7].annotations.title" == "Get Schema Metadata" +jsonpath "$.result.tools[7].annotations.readOnlyHint" == true +jsonpath "$.result.tools[7].annotations.destructiveHint" == false +jsonpath "$.result.tools[7].annotations.idempotentHint" == true +jsonpath "$.result.tools[7].annotations.openWorldHint" == false jsonpath "$.result.tools[8].name" == "evaluate_schema" jsonpath "$.result.tools[8].annotations.title" == "Evaluate Schema" +jsonpath "$.result.tools[8].annotations.readOnlyHint" == true +jsonpath "$.result.tools[8].annotations.destructiveHint" == false +jsonpath "$.result.tools[8].annotations.idempotentHint" == true +jsonpath "$.result.tools[8].annotations.openWorldHint" == false jsonpath "$.result.tools[9].name" == "trace_schema_evaluation" jsonpath "$.result.tools[9].annotations.title" == "Trace Schema Evaluation" +jsonpath "$.result.tools[9].annotations.readOnlyHint" == true +jsonpath "$.result.tools[9].annotations.destructiveHint" == false +jsonpath "$.result.tools[9].annotations.idempotentHint" == true +jsonpath "$.result.tools[9].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false # Added in 2025-06-18 ("outputSchema" on Tool) # https://modelcontextprotocol.io/specification/2025-06-18/changelog jsonpath "$..outputSchema" not exists diff --git a/enterprise/e2e/path/hurl/mcp-2025-06-18.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-06-18.all.hurl index 0d393b5d..49069053 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-06-18.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-06-18.all.hurl @@ -167,6 +167,10 @@ jsonpath "$.result.tools[0].description" == "List the contents of a directory in jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/list/response" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false # Added in 2025-11-25 (SEP-973) # https://github.com/modelcontextprotocol/modelcontextprotocol/issues/973 jsonpath "$..icons" not exists diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-gzip.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-gzip.all.hurl index efdade08..a337a49b 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-11-25-gzip.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-gzip.all.hurl @@ -160,8 +160,16 @@ jsonpath "$.id" == "tools-identity" jsonpath "$.result.tools" count == 11 jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/v1/catalog/self/v1/api/schemas/evaluate{{schema_path}} ``` @@ -192,8 +200,16 @@ jsonpath "$.id" == "tools-gzip" jsonpath "$.result.tools" count == 11 jsonpath "$.result.tools[0].name" == "list_directory" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/v1/catalog/self/v1/api/schemas/evaluate{{schema_path}} ``` diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl index aa37cfd3..40b92fc8 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-resources.all.hurl @@ -223,7 +223,7 @@ jsonpath "$.result.resources[40].uri" == "{{base}}/v1/catalog/self/v1/schemas/mc jsonpath "$.result.resources[40].name" == "response" jsonpath "$.result.resources[40].description" == "Any outgoing MCP response the server returns" jsonpath "$.result.resources[40].mimeType" == "application/schema+json" -jsonpath "$.result.resources[40].size" == 2976 +jsonpath "$.result.resources[40].size" == 3128 jsonpath "$.result.resources[41].uri" == "{{base}}/v1/catalog/self/v1/schemas/mcp/tools/call/request" jsonpath "$.result.resources[41].name" == "request" jsonpath "$.result.resources[41].description" == "Request from a client to invoke an MCP tool by name" @@ -243,7 +243,7 @@ jsonpath "$.result.resources[44].uri" == "{{base}}/v1/catalog/self/v1/schemas/mc jsonpath "$.result.resources[44].name" == "response" jsonpath "$.result.resources[44].description" == "Sourcemeta One's list of MCP tools" jsonpath "$.result.resources[44].mimeType" == "application/schema+json" -jsonpath "$.result.resources[44].size" == 3117 +jsonpath "$.result.resources[44].size" == 3831 jsonpath "$.result.resources[45].uri" == "{{base}}/v1/catalog/example/full" jsonpath "$.result.resources[45].name" == "full" jsonpath "$.result.resources[45].description" == "A schema that has both a title and a description" diff --git a/enterprise/e2e/path/hurl/mcp-2025-11-25-tools.all.hurl b/enterprise/e2e/path/hurl/mcp-2025-11-25-tools.all.hurl index b2efb940..be5161ed 100644 --- a/enterprise/e2e/path/hurl/mcp-2025-11-25-tools.all.hurl +++ b/enterprise/e2e/path/hurl/mcp-2025-11-25-tools.all.hurl @@ -24,56 +24,100 @@ jsonpath "$.result.tools[0].description" == "List the contents of a directory in jsonpath "$.result.tools[0].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/list/rpc" jsonpath "$.result.tools[0].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/list/response" jsonpath "$.result.tools[0].annotations.title" == "List Directory" +jsonpath "$.result.tools[0].annotations.readOnlyHint" == true +jsonpath "$.result.tools[0].annotations.destructiveHint" == false +jsonpath "$.result.tools[0].annotations.idempotentHint" == true +jsonpath "$.result.tools[0].annotations.openWorldHint" == false jsonpath "$.result.tools[1].name" == "get_schema_dependencies" jsonpath "$.result.tools[1].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[1].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/dependencies/rpc" jsonpath "$.result.tools[1].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/dependencies/response" jsonpath "$.result.tools[1].annotations.title" == "Get Schema Dependencies" +jsonpath "$.result.tools[1].annotations.readOnlyHint" == true +jsonpath "$.result.tools[1].annotations.destructiveHint" == false +jsonpath "$.result.tools[1].annotations.idempotentHint" == true +jsonpath "$.result.tools[1].annotations.openWorldHint" == false jsonpath "$.result.tools[2].name" == "get_schema_dependents" jsonpath "$.result.tools[2].description" == "Look up the dependency graph of a specific schema (incoming or outgoing)" jsonpath "$.result.tools[2].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/dependents/rpc" jsonpath "$.result.tools[2].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/dependents/response" jsonpath "$.result.tools[2].annotations.title" == "Get Schema Dependents" +jsonpath "$.result.tools[2].annotations.readOnlyHint" == true +jsonpath "$.result.tools[2].annotations.destructiveHint" == false +jsonpath "$.result.tools[2].annotations.idempotentHint" == true +jsonpath "$.result.tools[2].annotations.openWorldHint" == false jsonpath "$.result.tools[3].name" == "get_schema_health" jsonpath "$.result.tools[3].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[3].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/health/rpc" jsonpath "$.result.tools[3].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/health/response" jsonpath "$.result.tools[3].annotations.title" == "Get Schema Health" +jsonpath "$.result.tools[3].annotations.readOnlyHint" == true +jsonpath "$.result.tools[3].annotations.destructiveHint" == false +jsonpath "$.result.tools[3].annotations.idempotentHint" == true +jsonpath "$.result.tools[3].annotations.openWorldHint" == false jsonpath "$.result.tools[4].name" == "get_schema_locations" jsonpath "$.result.tools[4].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[4].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/locations/rpc" jsonpath "$.result.tools[4].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/locations/response" jsonpath "$.result.tools[4].annotations.title" == "Get Schema Locations" +jsonpath "$.result.tools[4].annotations.readOnlyHint" == true +jsonpath "$.result.tools[4].annotations.destructiveHint" == false +jsonpath "$.result.tools[4].annotations.idempotentHint" == true +jsonpath "$.result.tools[4].annotations.openWorldHint" == false jsonpath "$.result.tools[5].name" == "get_schema_positions" jsonpath "$.result.tools[5].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[5].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/positions/rpc" jsonpath "$.result.tools[5].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/positions/response" jsonpath "$.result.tools[5].annotations.title" == "Get Schema Positions" +jsonpath "$.result.tools[5].annotations.readOnlyHint" == true +jsonpath "$.result.tools[5].annotations.destructiveHint" == false +jsonpath "$.result.tools[5].annotations.idempotentHint" == true +jsonpath "$.result.tools[5].annotations.openWorldHint" == false jsonpath "$.result.tools[6].name" == "get_schema_stats" jsonpath "$.result.tools[6].description" == "Look up a precomputed artifact about a specific schema by its absolute URI" jsonpath "$.result.tools[6].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/stats/rpc" jsonpath "$.result.tools[6].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/stats/response" jsonpath "$.result.tools[6].annotations.title" == "Get Schema Stats" +jsonpath "$.result.tools[6].annotations.readOnlyHint" == true +jsonpath "$.result.tools[6].annotations.destructiveHint" == false +jsonpath "$.result.tools[6].annotations.idempotentHint" == true +jsonpath "$.result.tools[6].annotations.openWorldHint" == false jsonpath "$.result.tools[7].name" == "get_schema_metadata" jsonpath "$.result.tools[7].description" == "Read a navigation artifact for browsing schemas" jsonpath "$.result.tools[7].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/metadata/rpc" jsonpath "$.result.tools[7].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/metadata/response" jsonpath "$.result.tools[7].annotations.title" == "Get Schema Metadata" +jsonpath "$.result.tools[7].annotations.readOnlyHint" == true +jsonpath "$.result.tools[7].annotations.destructiveHint" == false +jsonpath "$.result.tools[7].annotations.idempotentHint" == true +jsonpath "$.result.tools[7].annotations.openWorldHint" == false jsonpath "$.result.tools[8].name" == "evaluate_schema" jsonpath "$.result.tools[8].description" == "Validate a JSON instance against a schema and return whether it is valid plus any errors" jsonpath "$.result.tools[8].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/evaluate/rpc" jsonpath "$.result.tools[8].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/evaluate/response" jsonpath "$.result.tools[8].annotations.title" == "Evaluate Schema" +jsonpath "$.result.tools[8].annotations.readOnlyHint" == true +jsonpath "$.result.tools[8].annotations.destructiveHint" == false +jsonpath "$.result.tools[8].annotations.idempotentHint" == true +jsonpath "$.result.tools[8].annotations.openWorldHint" == false jsonpath "$.result.tools[9].name" == "trace_schema_evaluation" jsonpath "$.result.tools[9].description" == "Validate a JSON instance against a schema and return detailed information about every step of the evaluation process" jsonpath "$.result.tools[9].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/trace/rpc" jsonpath "$.result.tools[9].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/trace/response" jsonpath "$.result.tools[9].annotations.title" == "Trace Schema Evaluation" +jsonpath "$.result.tools[9].annotations.readOnlyHint" == true +jsonpath "$.result.tools[9].annotations.destructiveHint" == false +jsonpath "$.result.tools[9].annotations.idempotentHint" == true +jsonpath "$.result.tools[9].annotations.openWorldHint" == false jsonpath "$.result.tools[10].name" == "search_schemas" jsonpath "$.result.tools[10].description" == "Search for schemas by query term" jsonpath "$.result.tools[10].inputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/search/rpc" jsonpath "$.result.tools[10].outputSchema['$ref']" == "{{base}}/v1/catalog/self/v1/schemas/api/schemas/search/response" jsonpath "$.result.tools[10].annotations.title" == "Search Schemas" +jsonpath "$.result.tools[10].annotations.readOnlyHint" == true +jsonpath "$.result.tools[10].annotations.destructiveHint" == false +jsonpath "$.result.tools[10].annotations.idempotentHint" == true +jsonpath "$.result.tools[10].annotations.openWorldHint" == false POST {{base}}/v1/catalog/self/v1/api/schemas/evaluate{{schema_path}} ``` diff --git a/enterprise/index/enterprise_index.cc b/enterprise/index/enterprise_index.cc index 0756e9d9..a6e51065 100644 --- a/enterprise/index/enterprise_index.cc +++ b/enterprise/index/enterprise_index.cc @@ -155,18 +155,32 @@ auto generate_mcp_tools(const sourcemeta::core::URITemplateRouterView &router, output_schema_ref.emplace(std::move(ref)); } - auto annotations{sourcemeta::core::JSON::make_object()}; std::string title{operation_id}; sourcemeta::core::to_title_case(title); - annotations.assign("title", sourcemeta::core::JSON{title}); tool_routes.assign( std::string{operation_id}, sourcemeta::core::JSON{static_cast(identifier)}); - tools.push_back(sourcemeta::one::mcp_make_tool_descriptor( - sourcemeta::one::MCPProtocolVersion::V_2025_11_25, operation_id, - description, std::move(input_schema_ref), std::move(output_schema_ref), - std::move(annotations))); + + auto tool_entry{sourcemeta::core::JSON::make_array()}; + tool_entry.push_back(sourcemeta::core::JSON{operation_id}); + tool_entry.push_back(sourcemeta::core::JSON{description}); + tool_entry.push_back(std::move(input_schema_ref)); + if (output_schema_ref.has_value()) { + tool_entry.push_back(std::move(output_schema_ref).value()); + } else { + tool_entry.push_back(sourcemeta::core::JSON{nullptr}); + } + tool_entry.push_back(sourcemeta::core::JSON{title}); + tool_entry.push_back( + sourcemeta::core::JSON{sourcemeta::one::is_read_only(context)}); + tool_entry.push_back( + sourcemeta::core::JSON{sourcemeta::one::is_destructive(context)}); + tool_entry.push_back( + sourcemeta::core::JSON{sourcemeta::one::is_idempotent(context)}); + tool_entry.push_back( + sourcemeta::core::JSON{sourcemeta::one::is_open_world(context)}); + tools.push_back(std::move(tool_entry)); } } diff --git a/enterprise/server/action_mcp_v1.cc b/enterprise/server/action_mcp_v1.cc index c5e7eca6..54bdbc28 100644 --- a/enterprise/server/action_mcp_v1.cc +++ b/enterprise/server/action_mcp_v1.cc @@ -9,17 +9,39 @@ #include #include -#include // std::exception -#include // std::filesystem -#include // std::ostringstream -#include // std::string -#include // std::string_view -#include // std::move +#include // assert +#include // std::from_chars +#include // std::size_t +#include // std::exception +#include // std::filesystem +#include // std::optional, std::nullopt +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::errc +#include // std::move namespace { constexpr std::string_view MCP_TEMPLATE_MIME_TYPE{"application/schema+json"}; +auto parse_cursor_as_unsigned_integer(const std::string_view cursor) + -> std::optional { + if (cursor.empty()) { + return std::nullopt; + } + if (cursor.size() > 1 && cursor.front() == '0') { + return std::nullopt; + } + std::size_t parsed{0}; + const auto [end, error]{ + std::from_chars(cursor.data(), cursor.data() + cursor.size(), parsed)}; + if (error != std::errc{} || end != cursor.data() + cursor.size()) { + return std::nullopt; + } + return parsed; +} + } // namespace namespace sourcemeta::one::enterprise { @@ -33,8 +55,7 @@ auto ActionMCP_v1::on_resources_list(const sourcemeta::core::JSON &request_json) if (params != nullptr && params->defines("cursor")) { const auto &cursor_string{params->at("cursor").to_string()}; if (!cursor_string.empty()) { - const auto parsed{ - sourcemeta::one::mcp_parse_cursor_as_unsigned_integer(cursor_string)}; + const auto parsed{parse_cursor_as_unsigned_integer(cursor_string)}; if (!parsed.has_value()) { return sourcemeta::core::jsonrpc_make_error_invalid_params( id, sourcemeta::core::JSON{cursor_string}); @@ -59,8 +80,22 @@ auto ActionMCP_v1::on_initialize(const sourcemeta::core::JSON &request_json) const auto &parts{ this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_INITIALIZE)}; return sourcemeta::one::mcp_make_initialize_result( - request_json, parts.at("capabilities"), parts.at("serverInfo"), - parts.at("instructions").to_string()); + request_json, + sourcemeta::one::MCPServerCapabilities{ + .prompts = parts.at(0).to_boolean(), + .resources = parts.at(1).to_boolean(), + .tools = parts.at(2).to_boolean(), + .logging = parts.at(3).to_boolean(), + .completions = parts.at(4).to_boolean(), + }, + sourcemeta::one::MCPImplementation{ + .name = parts.at(5).to_string(), + .version = parts.at(6).to_string(), + .title = parts.at(7).to_string(), + .description = parts.at(8).to_string(), + .website_url = parts.at(9).to_string(), + }, + parts.at(10).to_string()); } auto ActionMCP_v1::on_tools_list( @@ -69,25 +104,23 @@ auto ActionMCP_v1::on_tools_list( -> sourcemeta::core::JSON { const auto &precomputed{ this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_TOOLS_LIST)}; - if (sourcemeta::one::mcp_supports_output_schema(version)) { - return sourcemeta::core::jsonrpc_make_success(request_json.at("id"), - precomputed); - } auto tools{sourcemeta::core::JSON::make_array()}; - for (const auto &tool : precomputed.at("tools").as_array()) { + for (const auto &tool : precomputed.as_array()) { std::optional output_schema; - if (tool.defines("outputSchema")) { - output_schema = tool.at("outputSchema"); - } - std::optional annotations; - if (tool.defines("annotations")) { - annotations = tool.at("annotations"); + if (!tool.at(3).is_null()) { + output_schema = tool.at(3); } tools.push_back(sourcemeta::one::mcp_make_tool_descriptor( - version, tool.at("name").to_string(), - tool.at("description").to_string(), tool.at("inputSchema"), - std::move(output_schema), std::move(annotations))); + version, tool.at(0).to_string(), tool.at(1).to_string(), tool.at(2), + std::move(output_schema), + sourcemeta::one::MCPToolAnnotations{ + .title = tool.at(4).to_string(), + .read_only = tool.at(5).to_boolean(), + .destructive = tool.at(6).to_boolean(), + .idempotent = tool.at(7).to_boolean(), + .open_world = tool.at(8).to_boolean(), + })); } auto result{sourcemeta::core::JSON::make_object()}; result.assign_assume_new(std::string{"tools"}, std::move(tools)); @@ -219,13 +252,14 @@ auto ActionMCP_v1::on_message(const sourcemeta::one::MCPProtocolVersion version, return; } - const auto &id{request_json.at("id")}; + const auto *id{sourcemeta::core::jsonrpc_request_id(request_json)}; + assert(id != nullptr); const auto method{sourcemeta::core::jsonrpc_method(request_json)}; sourcemeta::core::JSON envelope{nullptr}; if (!sourcemeta::one::mcp_is_request_method(method)) { - envelope = sourcemeta::core::jsonrpc_make_error_method_not_found(id); + envelope = sourcemeta::core::jsonrpc_make_error_method_not_found(*id); } else if (!this->validate(this->request_schema_, request_json)) { - envelope = sourcemeta::core::jsonrpc_make_error_invalid_request(&id); + envelope = sourcemeta::core::jsonrpc_make_error_invalid_request(id); } else if (method == sourcemeta::one::MCP_METHOD_INITIALIZE) { envelope = this->on_initialize(request_json); } else if (method == sourcemeta::one::MCP_METHOD_TOOLS_LIST) { @@ -238,9 +272,9 @@ auto ActionMCP_v1::on_message(const sourcemeta::one::MCPProtocolVersion version, envelope = this->on_tools_call(version, request_json, body); } else if (this->mcp_metadata_.defines(method)) { envelope = sourcemeta::core::jsonrpc_make_success( - id, this->mcp_metadata_.at(method)); + *id, this->mcp_metadata_.at(method)); } else { - envelope = sourcemeta::core::jsonrpc_make_success_empty(id); + envelope = sourcemeta::core::jsonrpc_make_success_empty(*id); } this->write_envelope(request, response, sourcemeta::one::STATUS_OK, envelope); diff --git a/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h b/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h index 89a6613f..d763b5b2 100644 --- a/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h +++ b/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h @@ -25,6 +25,10 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Handle Model Context Protocol JSON-RPC requests"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionMCP_v1(const std::filesystem::path &base, const sourcemeta::core::URITemplateRouterView &router, diff --git a/src/actions/action_default_v1.h b/src/actions/action_default_v1.h index 66a6a747..2b31d938 100644 --- a/src/actions/action_default_v1.h +++ b/src/actions/action_default_v1.h @@ -21,6 +21,10 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Default fallback action for unmatched URIs"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionDefault_v1( const std::filesystem::path &base, diff --git a/src/actions/action_dependency_tree_v1.h b/src/actions/action_dependency_tree_v1.h index cb7a5e5a..6fa2ca6d 100644 --- a/src/actions/action_dependency_tree_v1.h +++ b/src/actions/action_dependency_tree_v1.h @@ -26,6 +26,10 @@ class ActionDependencyTree_v1 : public sourcemeta::one::RouterAction { static constexpr std::string_view DESCRIPTION{ "Look up the dependency graph of a specific schema (incoming or " "outgoing)"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionDependencyTree_v1( const std::filesystem::path &base, diff --git a/src/actions/action_health_check_v1.h b/src/actions/action_health_check_v1.h index 05f2ac28..757fa718 100644 --- a/src/actions/action_health_check_v1.h +++ b/src/actions/action_health_check_v1.h @@ -17,6 +17,10 @@ class ActionHealthCheck_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Report the server's health status"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionHealthCheck_v1( const std::filesystem::path &base, diff --git a/src/actions/action_jsonschema_evaluate_v1.h b/src/actions/action_jsonschema_evaluate_v1.h index 1aaf4b4c..7330b77f 100644 --- a/src/actions/action_jsonschema_evaluate_v1.h +++ b/src/actions/action_jsonschema_evaluate_v1.h @@ -30,6 +30,10 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::RouterAction { static constexpr std::string_view DESCRIPTION{ "Validate a JSON instance against a schema and return whether it " "is valid plus any errors"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionJSONSchemaEvaluate_v1( const std::filesystem::path &base, diff --git a/src/actions/action_jsonschema_trace_v1.h b/src/actions/action_jsonschema_trace_v1.h index f93d07a0..6b0bbb8e 100644 --- a/src/actions/action_jsonschema_trace_v1.h +++ b/src/actions/action_jsonschema_trace_v1.h @@ -33,6 +33,10 @@ class ActionJSONSchemaTrace_v1 : public sourcemeta::one::RouterAction { static constexpr std::string_view DESCRIPTION{ "Validate a JSON instance against a schema and return detailed " "information about every step of the evaluation process"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionJSONSchemaTrace_v1( const std::filesystem::path &base, diff --git a/src/actions/action_list_directory_v1.h b/src/actions/action_list_directory_v1.h index 07f39df0..a0fe6c22 100644 --- a/src/actions/action_list_directory_v1.h +++ b/src/actions/action_list_directory_v1.h @@ -24,6 +24,10 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "List the contents of a directory in the catalog"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionListDirectory_v1( const std::filesystem::path &base, diff --git a/src/actions/action_mcp_v1.h b/src/actions/action_mcp_v1.h index 108e0b7a..43a43f63 100644 --- a/src/actions/action_mcp_v1.h +++ b/src/actions/action_mcp_v1.h @@ -35,6 +35,10 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Handle Model Context Protocol JSON-RPC requests"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionMCP_v1(const std::filesystem::path &base, const sourcemeta::core::URITemplateRouterView &router, @@ -195,8 +199,22 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { const auto &parts{ this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_INITIALIZE)}; return sourcemeta::one::mcp_make_initialize_result( - request_json, parts.at("capabilities"), parts.at("serverInfo"), - parts.at("instructions").to_string()); + request_json, + sourcemeta::one::MCPServerCapabilities{ + .prompts = parts.at(0).to_boolean(), + .resources = parts.at(1).to_boolean(), + .tools = parts.at(2).to_boolean(), + .logging = parts.at(3).to_boolean(), + .completions = parts.at(4).to_boolean(), + }, + sourcemeta::one::MCPImplementation{ + .name = parts.at(5).to_string(), + .version = parts.at(6).to_string(), + .title = parts.at(7).to_string(), + .description = parts.at(8).to_string(), + .website_url = parts.at(9).to_string(), + }, + parts.at(10).to_string()); } if (method == sourcemeta::one::MCP_METHOD_RESOURCES_TEMPLATES_LIST) { diff --git a/src/actions/action_not_found_v1.h b/src/actions/action_not_found_v1.h index 56745b01..5200a2f3 100644 --- a/src/actions/action_not_found_v1.h +++ b/src/actions/action_not_found_v1.h @@ -17,6 +17,10 @@ class ActionNotFound_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Return a 404 Not Found response"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionNotFound_v1( const std::filesystem::path &base, diff --git a/src/actions/action_schema_search_v1.h b/src/actions/action_schema_search_v1.h index 8aced568..27ebee00 100644 --- a/src/actions/action_schema_search_v1.h +++ b/src/actions/action_schema_search_v1.h @@ -26,6 +26,10 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Search for schemas by query term"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionSchemaSearch_v1( const std::filesystem::path &base, diff --git a/src/actions/action_serve_explorer_artifact_v1.h b/src/actions/action_serve_explorer_artifact_v1.h index 4bfcd319..45660549 100644 --- a/src/actions/action_serve_explorer_artifact_v1.h +++ b/src/actions/action_serve_explorer_artifact_v1.h @@ -23,6 +23,10 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Read a navigation artifact for browsing schemas"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionServeExplorerArtifact_v1( const std::filesystem::path &base, diff --git a/src/actions/action_serve_schema_artifact_v1.h b/src/actions/action_serve_schema_artifact_v1.h index 95cba865..4ada3d4c 100644 --- a/src/actions/action_serve_schema_artifact_v1.h +++ b/src/actions/action_serve_schema_artifact_v1.h @@ -24,6 +24,10 @@ class ActionServeSchemaArtifact_v1 : public sourcemeta::one::RouterAction { static constexpr std::string_view DESCRIPTION{ "Look up a precomputed artifact about a specific schema by its " "absolute URI"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionServeSchemaArtifact_v1( const std::filesystem::path &base, diff --git a/src/actions/action_serve_static_v1.h b/src/actions/action_serve_static_v1.h index 67b72b28..fb112e92 100644 --- a/src/actions/action_serve_static_v1.h +++ b/src/actions/action_serve_static_v1.h @@ -19,6 +19,10 @@ class ActionServeStatic_v1 : public sourcemeta::one::RouterAction { public: static constexpr std::string_view DESCRIPTION{ "Serve a static asset bundled with the server"}; + static constexpr bool READ_ONLY{true}; + static constexpr bool DESTRUCTIVE{false}; + static constexpr bool IDEMPOTENT{true}; + static constexpr bool OPEN_WORLD{false}; ActionServeStatic_v1( const std::filesystem::path &base, diff --git a/src/actions/actions.cc b/src/actions/actions.cc index d373a7b2..5fd6adae 100644 --- a/src/actions/actions.cc +++ b/src/actions/actions.cc @@ -1,5 +1,8 @@ #include +#include // assert +#include // std::string_view + #include "action_default_v1.h" #include "action_dependency_tree_v1.h" #include "action_health_check_v1.h" @@ -13,6 +16,27 @@ #include "action_serve_schema_artifact_v1.h" #include "action_serve_static_v1.h" +namespace { + +struct ActionMetadata { + std::string_view description; + bool read_only; + bool destructive; + bool idempotent; + bool open_world; +}; + +#define SOURCEMETA_ONE_DEFINE_METADATA(Name, Class) \ + ActionMetadata{Class::DESCRIPTION, Class::READ_ONLY, Class::DESTRUCTIVE, \ + Class::IDEMPOTENT, Class::OPEN_WORLD}, + +const std::array METADATA{ + {SOURCEMETA_ONE_FOR_EACH_ACTION(SOURCEMETA_ONE_DEFINE_METADATA)}}; + +#undef SOURCEMETA_ONE_DEFINE_METADATA + +} // namespace + namespace sourcemeta::one { #define SOURCEMETA_ONE_MAKE_CONSTRUCTOR_ENTRY(Name, Class) \ @@ -26,20 +50,41 @@ const std::array CONSTRUCTORS{[] { #undef SOURCEMETA_ONE_MAKE_CONSTRUCTOR_ENTRY -#define SOURCEMETA_ONE_DEFINE_DESCRIPTION(Name, Class) Class::DESCRIPTION, - -const std::array DESCRIPTIONS{ - {SOURCEMETA_ONE_FOR_EACH_ACTION(SOURCEMETA_ONE_DEFINE_DESCRIPTION)}}; - -#undef SOURCEMETA_ONE_DEFINE_DESCRIPTION - auto action_description( const sourcemeta::core::URITemplateRouter::Identifier context) noexcept -> std::string_view { - if (context >= DESCRIPTIONS.size()) { + if (context >= METADATA.size()) { return {}; } - return DESCRIPTIONS[context]; + return METADATA[context].description; +} + +auto is_read_only( + const sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool { + assert(context < METADATA.size()); + return METADATA[context].read_only; +} + +auto is_destructive( + const sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool { + assert(context < METADATA.size()); + return METADATA[context].destructive; +} + +auto is_idempotent( + const sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool { + assert(context < METADATA.size()); + return METADATA[context].idempotent; +} + +auto is_open_world( + const sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool { + assert(context < METADATA.size()); + return METADATA[context].open_world; } } // namespace sourcemeta::one diff --git a/src/actions/include/sourcemeta/one/actions.h b/src/actions/include/sourcemeta/one/actions.h index 1058d303..f5f266c3 100644 --- a/src/actions/include/sourcemeta/one/actions.h +++ b/src/actions/include/sourcemeta/one/actions.h @@ -34,12 +34,27 @@ enum : std::uint8_t { extern const std::array CONSTRUCTORS; -extern const std::array DESCRIPTIONS; [[nodiscard]] auto action_description( sourcemeta::core::URITemplateRouter::Identifier context) noexcept -> std::string_view; +[[nodiscard]] auto +is_read_only(sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool; + +[[nodiscard]] auto +is_destructive(sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool; + +[[nodiscard]] auto +is_idempotent(sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool; + +[[nodiscard]] auto +is_open_world(sourcemeta::core::URITemplateRouter::Identifier context) noexcept + -> bool; + } // namespace sourcemeta::one #endif diff --git a/src/index/explorer.h b/src/index/explorer.h index eb1c0774..06c4a70d 100644 --- a/src/index/explorer.h +++ b/src/index/explorer.h @@ -559,18 +559,13 @@ struct GENERATE_MCP { const sourcemeta::core::JSON &) -> void { const auto timestamp_start{std::chrono::steady_clock::now()}; - auto server_info{sourcemeta::core::JSON::make_object()}; #if defined(SOURCEMETA_ONE_ENTERPRISE) - server_info.assign("name", - sourcemeta::core::JSON{"sourcemeta-one-enterprise"}); - server_info.assign("title", - sourcemeta::core::JSON{"Sourcemeta One Enterprise"}); + constexpr std::string_view SERVER_NAME{"sourcemeta-one-enterprise"}; + constexpr std::string_view SERVER_TITLE{"Sourcemeta One Enterprise"}; #else - server_info.assign("name", sourcemeta::core::JSON{"sourcemeta-one"}); - server_info.assign("title", sourcemeta::core::JSON{"Sourcemeta One"}); + constexpr std::string_view SERVER_NAME{"sourcemeta-one"}; + constexpr std::string_view SERVER_TITLE{"Sourcemeta One"}; #endif - server_info.assign("version", - sourcemeta::core::JSON{sourcemeta::one::version()}); constexpr std::string_view INSTRUCTIONS_BODY{ "Sourcemeta One is a JSON Schema registry. It serves a catalog of " @@ -616,25 +611,25 @@ struct GENERATE_MCP { } #endif - auto capabilities{sourcemeta::core::JSON::make_object()}; - capabilities.assign("resources", sourcemeta::core::JSON::make_object()); - if (!tools.empty()) { - capabilities.assign("tools", sourcemeta::core::JSON::make_object()); - } - - auto initialize_ingredients{sourcemeta::core::JSON::make_object()}; - initialize_ingredients.assign("capabilities", std::move(capabilities)); - initialize_ingredients.assign("serverInfo", std::move(server_info)); - initialize_ingredients.assign("instructions", - sourcemeta::core::JSON{instructions.str()}); + auto initialize_ingredients{sourcemeta::core::JSON::make_array()}; + initialize_ingredients.push_back(sourcemeta::core::JSON{false}); + initialize_ingredients.push_back(sourcemeta::core::JSON{true}); + initialize_ingredients.push_back(sourcemeta::core::JSON{!tools.empty()}); + initialize_ingredients.push_back(sourcemeta::core::JSON{false}); + initialize_ingredients.push_back(sourcemeta::core::JSON{false}); + initialize_ingredients.push_back(sourcemeta::core::JSON{SERVER_NAME}); + initialize_ingredients.push_back( + sourcemeta::core::JSON{sourcemeta::one::version()}); + initialize_ingredients.push_back(sourcemeta::core::JSON{SERVER_TITLE}); + initialize_ingredients.push_back(sourcemeta::core::JSON{""}); + initialize_ingredients.push_back(sourcemeta::core::JSON{""}); + initialize_ingredients.push_back( + sourcemeta::core::JSON{instructions.str()}); auto resource_templates_response{sourcemeta::core::JSON::make_object()}; resource_templates_response.assign("resourceTemplates", std::move(resource_templates)); - auto tools_response{sourcemeta::core::JSON::make_object()}; - tools_response.assign("tools", std::move(tools)); - auto document{sourcemeta::core::JSON::make_object()}; document.assign("origin", sourcemeta::core::JSON{configuration.origin}); document.assign(std::string{sourcemeta::one::MCP_METHOD_INITIALIZE}, @@ -644,7 +639,7 @@ struct GENERATE_MCP { std::move(resource_templates_response)); document.assign("resources", std::move(resources)); document.assign(std::string{sourcemeta::one::MCP_METHOD_TOOLS_LIST}, - std::move(tools_response)); + std::move(tools)); document.assign("toolRoutes", std::move(tool_routes)); const auto timestamp_end{std::chrono::steady_clock::now()}; diff --git a/src/mcp/include/sourcemeta/one/mcp.h b/src/mcp/include/sourcemeta/one/mcp.h index c6bc23a9..cf5e372e 100644 --- a/src/mcp/include/sourcemeta/one/mcp.h +++ b/src/mcp/include/sourcemeta/one/mcp.h @@ -44,72 +44,95 @@ constexpr std::string_view MCP_METHOD_NOTIFICATIONS_INITIALIZED{ "notifications/initialized"}; constexpr std::int64_t MCP_CODE_RESOURCE_NOT_FOUND{-32002}; +constexpr std::int64_t MCP_CODE_URL_ELICITATION_REQUIRED{-32042}; -auto mcp_make_text_block(std::string_view text) -> sourcemeta::core::JSON; +auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; -auto mcp_make_resource_link(MCPProtocolVersion version, std::string_view uri, - std::string_view mime_type, - std::string_view name = {}, - std::string_view description = {}) +auto mcp_make_resource_link(const MCPProtocolVersion version, + const std::string_view uri, + const std::string_view mime_type, + const std::string_view name = {}, + const std::string_view description = {}) -> sourcemeta::core::JSON; -auto mcp_make_tool_success(MCPProtocolVersion version, +auto mcp_make_tool_success(const MCPProtocolVersion version, const sourcemeta::core::JSON &id, sourcemeta::core::JSON result) -> sourcemeta::core::JSON; -auto mcp_make_tool_success(MCPProtocolVersion version, +auto mcp_make_tool_success(const MCPProtocolVersion version, const sourcemeta::core::JSON &id, sourcemeta::core::JSON structured, sourcemeta::core::JSON content_blocks) -> sourcemeta::core::JSON; auto mcp_make_tool_error(const sourcemeta::core::JSON &id, - std::string_view message) -> sourcemeta::core::JSON; + const std::string_view message) + -> sourcemeta::core::JSON; auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &id, - std::string_view uri) + const std::string_view uri) -> sourcemeta::core::JSON; -auto mcp_make_resource(std::string_view uri, std::string_view name, - std::string_view mime_type, - std::string_view description = {}, - std::optional size = std::nullopt) +auto mcp_make_resource(const std::string_view uri, const std::string_view name, + const std::string_view mime_type, + const std::string_view description = {}, + const std::optional size = std::nullopt) -> sourcemeta::core::JSON; -auto mcp_make_resource_text_content(std::string_view uri, - std::string_view mime_type, - std::string_view text) +auto mcp_make_resource_text_content(const std::string_view uri, + const std::string_view mime_type, + const std::string_view text) -> sourcemeta::core::JSON; auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) -> sourcemeta::core::JSON; -auto mcp_make_resource_template(std::string_view uri_template, - std::string_view name, - std::string_view description, - std::string_view mime_type) +auto mcp_make_resource_template(const std::string_view uri_template, + const std::string_view name, + const std::string_view description, + const std::string_view mime_type) -> sourcemeta::core::JSON; +struct MCPToolAnnotations { + std::string_view title = {}; + bool read_only = false; + bool destructive = true; + bool idempotent = false; + bool open_world = true; +}; + auto mcp_make_tool_descriptor( - MCPProtocolVersion version, std::string_view name, - std::string_view description, sourcemeta::core::JSON input_schema, + const MCPProtocolVersion version, const std::string_view name, + const std::string_view description, sourcemeta::core::JSON input_schema, std::optional output_schema = std::nullopt, - std::optional annotations = std::nullopt) - -> sourcemeta::core::JSON; + const MCPToolAnnotations &annotations = {}) -> sourcemeta::core::JSON; + +struct MCPImplementation { + std::string_view name; + std::string_view version; + std::string_view title = {}; + std::string_view description = {}; + std::string_view website_url = {}; +}; + +struct MCPServerCapabilities { + bool prompts = false; + bool resources = false; + bool tools = false; + bool logging = false; + bool completions = false; +}; auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, - sourcemeta::core::JSON capabilities, - sourcemeta::core::JSON server_info, - std::string_view instructions = {}) + const MCPServerCapabilities &capabilities, + const MCPImplementation &server, + const std::string_view instructions = {}) -> sourcemeta::core::JSON; auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON *; -auto mcp_parse_cursor_as_unsigned_integer(std::string_view cursor) - -> std::optional; - constexpr auto mcp_is_request_method(const std::string_view method) noexcept -> bool { return method == MCP_METHOD_INITIALIZE || method == MCP_METHOD_PING || @@ -160,6 +183,16 @@ mcp_supports_implementation_title(const MCPProtocolVersion version) noexcept return version != MCPProtocolVersion::V_2025_03_26; } +constexpr auto mcp_supports_implementation_description( + const MCPProtocolVersion version) noexcept -> bool { + return version == MCPProtocolVersion::V_2025_11_25; +} + +constexpr auto mcp_supports_implementation_website_url( + const MCPProtocolVersion version) noexcept -> bool { + return version == MCPProtocolVersion::V_2025_11_25; +} + } // namespace sourcemeta::one #endif diff --git a/src/mcp/mcp.cc b/src/mcp/mcp.cc index 9cd54fbe..e3b0db79 100644 --- a/src/mcp/mcp.cc +++ b/src/mcp/mcp.cc @@ -3,22 +3,93 @@ #include #include -#include // std::from_chars -#include // std::size_t -#include // std::optional -#include // std::ostringstream -#include // std::string -#include // std::string_view -#include // std::errc -#include // std::move +#include // assert +#include // std::size_t +#include // std::optional +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::move + +namespace { + +const auto MCP_HASH_ANNOTATIONS{ + sourcemeta::core::JSON::make_object().as_object().hash("annotations")}; +const auto MCP_HASH_ARGUMENTS{ + sourcemeta::core::JSON::make_object().as_object().hash("arguments")}; +const auto MCP_HASH_CAPABILITIES{ + sourcemeta::core::JSON::make_object().as_object().hash("capabilities")}; +const auto MCP_HASH_COMPLETIONS{ + sourcemeta::core::JSON::make_object().as_object().hash("completions")}; +const auto MCP_HASH_CONTENT{ + sourcemeta::core::JSON::make_object().as_object().hash("content")}; +const auto MCP_HASH_CONTENTS{ + sourcemeta::core::JSON::make_object().as_object().hash("contents")}; +const auto MCP_HASH_DESCRIPTION{ + sourcemeta::core::JSON::make_object().as_object().hash("description")}; +const auto MCP_HASH_DESTRUCTIVE_HINT{ + sourcemeta::core::JSON::make_object().as_object().hash("destructiveHint")}; +const auto MCP_HASH_IDEMPOTENT_HINT{ + sourcemeta::core::JSON::make_object().as_object().hash("idempotentHint")}; +const auto MCP_HASH_INPUT_SCHEMA{ + sourcemeta::core::JSON::make_object().as_object().hash("inputSchema")}; +const auto MCP_HASH_INSTRUCTIONS{ + sourcemeta::core::JSON::make_object().as_object().hash("instructions")}; +const auto MCP_HASH_IS_ERROR{ + sourcemeta::core::JSON::make_object().as_object().hash("isError")}; +const auto MCP_HASH_LOGGING{ + sourcemeta::core::JSON::make_object().as_object().hash("logging")}; +const auto MCP_HASH_MIME_TYPE{ + sourcemeta::core::JSON::make_object().as_object().hash("mimeType")}; +const auto MCP_HASH_NAME{ + sourcemeta::core::JSON::make_object().as_object().hash("name")}; +const auto MCP_HASH_OPEN_WORLD_HINT{ + sourcemeta::core::JSON::make_object().as_object().hash("openWorldHint")}; +const auto MCP_HASH_OUTPUT_SCHEMA{ + sourcemeta::core::JSON::make_object().as_object().hash("outputSchema")}; +const auto MCP_HASH_PROMPTS{ + sourcemeta::core::JSON::make_object().as_object().hash("prompts")}; +const auto MCP_HASH_PROTOCOL_VERSION{ + sourcemeta::core::JSON::make_object().as_object().hash("protocolVersion")}; +const auto MCP_HASH_READ_ONLY_HINT{ + sourcemeta::core::JSON::make_object().as_object().hash("readOnlyHint")}; +const auto MCP_HASH_RESOURCES{ + sourcemeta::core::JSON::make_object().as_object().hash("resources")}; +const auto MCP_HASH_SERVER_INFO{ + sourcemeta::core::JSON::make_object().as_object().hash("serverInfo")}; +const auto MCP_HASH_SIZE{ + sourcemeta::core::JSON::make_object().as_object().hash("size")}; +const auto MCP_HASH_STRUCTURED_CONTENT{ + sourcemeta::core::JSON::make_object().as_object().hash( + "structuredContent")}; +const auto MCP_HASH_TEXT{ + sourcemeta::core::JSON::make_object().as_object().hash("text")}; +const auto MCP_HASH_TITLE{ + sourcemeta::core::JSON::make_object().as_object().hash("title")}; +const auto MCP_HASH_TOOLS{ + sourcemeta::core::JSON::make_object().as_object().hash("tools")}; +const auto MCP_HASH_TYPE{ + sourcemeta::core::JSON::make_object().as_object().hash("type")}; +const auto MCP_HASH_URI{ + sourcemeta::core::JSON::make_object().as_object().hash("uri")}; +const auto MCP_HASH_URI_TEMPLATE{ + sourcemeta::core::JSON::make_object().as_object().hash("uriTemplate")}; +const auto MCP_HASH_VERSION{ + sourcemeta::core::JSON::make_object().as_object().hash("version")}; +const auto MCP_HASH_WEBSITE_URL{ + sourcemeta::core::JSON::make_object().as_object().hash("websiteUrl")}; + +} // namespace namespace sourcemeta::one { auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON { auto block{sourcemeta::core::JSON::make_object()}; - block.assign_assume_new(std::string{"type"}, sourcemeta::core::JSON{"text"}); - block.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}); + block.assign_assume_new(std::string{"type"}, sourcemeta::core::JSON{"text"}, + MCP_HASH_TYPE); + block.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, + MCP_HASH_TEXT); return block; } @@ -48,17 +119,22 @@ auto mcp_make_resource_link(const MCPProtocolVersion version, auto block{sourcemeta::core::JSON::make_object()}; block.assign_assume_new(std::string{"type"}, - sourcemeta::core::JSON{"resource_link"}); - block.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}); + sourcemeta::core::JSON{"resource_link"}, + MCP_HASH_TYPE); + block.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); if (!name.empty()) { - block.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}); + block.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); } if (!description.empty()) { block.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}); + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); } block.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}); + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); return block; } @@ -73,13 +149,15 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, content.push_back(mcp_make_text_block(payload.str())); auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new(std::string{"content"}, std::move(content)); + envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + MCP_HASH_CONTENT); if (mcp_supports_structured_content(version)) { envelope_result.assign_assume_new(std::string{"structuredContent"}, - std::move(result)); + std::move(result), + MCP_HASH_STRUCTURED_CONTENT); } - envelope_result.assign_assume_new(std::string{"isError"}, - sourcemeta::core::JSON{false}); + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(id, std::move(envelope_result)); } @@ -89,14 +167,15 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, sourcemeta::core::JSON content_blocks) -> sourcemeta::core::JSON { auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new(std::string{"content"}, - std::move(content_blocks)); + envelope_result.assign_assume_new( + std::string{"content"}, std::move(content_blocks), MCP_HASH_CONTENT); if (mcp_supports_structured_content(version)) { envelope_result.assign_assume_new(std::string{"structuredContent"}, - std::move(structured)); + std::move(structured), + MCP_HASH_STRUCTURED_CONTENT); } - envelope_result.assign_assume_new(std::string{"isError"}, - sourcemeta::core::JSON{false}); + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(id, std::move(envelope_result)); } @@ -107,9 +186,10 @@ auto mcp_make_tool_error(const sourcemeta::core::JSON &id, content.push_back(mcp_make_text_block(message)); auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new(std::string{"content"}, std::move(content)); - envelope_result.assign_assume_new(std::string{"isError"}, - sourcemeta::core::JSON{true}); + envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + MCP_HASH_CONTENT); + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{true}, MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(id, std::move(envelope_result)); } @@ -127,17 +207,22 @@ auto mcp_make_resource(const std::string_view uri, const std::string_view name, const std::optional size) -> sourcemeta::core::JSON { auto resource{sourcemeta::core::JSON::make_object()}; - resource.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}); - resource.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}); + resource.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); + resource.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); if (!description.empty()) { resource.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}); + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); } resource.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}); + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); if (size.has_value()) { resource.assign_assume_new(std::string{"size"}, - sourcemeta::core::JSON{size.value()}); + sourcemeta::core::JSON{size.value()}, + MCP_HASH_SIZE); } return resource; } @@ -149,12 +234,16 @@ auto mcp_make_resource_template(const std::string_view uri_template, -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; entry.assign_assume_new(std::string{"uriTemplate"}, - sourcemeta::core::JSON{uri_template}); - entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}); + sourcemeta::core::JSON{uri_template}, + MCP_HASH_URI_TEMPLATE); + entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); entry.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}); + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); entry.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}); + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); return entry; } @@ -162,27 +251,52 @@ auto mcp_make_tool_descriptor( const MCPProtocolVersion version, const std::string_view name, const std::string_view description, sourcemeta::core::JSON input_schema, std::optional output_schema, - std::optional annotations) - -> sourcemeta::core::JSON { + const MCPToolAnnotations &annotations) -> sourcemeta::core::JSON { + assert(!annotations.read_only || !annotations.destructive); + assert(!annotations.read_only || annotations.idempotent); + auto entry{sourcemeta::core::JSON::make_object()}; - entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}); + entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); entry.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}); - entry.assign_assume_new(std::string{"inputSchema"}, std::move(input_schema)); + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + entry.assign_assume_new(std::string{"inputSchema"}, std::move(input_schema), + MCP_HASH_INPUT_SCHEMA); if (output_schema.has_value() && mcp_supports_output_schema(version)) { entry.assign_assume_new(std::string{"outputSchema"}, - std::move(output_schema).value()); + std::move(output_schema).value(), + MCP_HASH_OUTPUT_SCHEMA); } - if (annotations.has_value()) { - entry.assign_assume_new(std::string{"annotations"}, - std::move(annotations).value()); + + auto annotations_object{sourcemeta::core::JSON::make_object()}; + if (!annotations.title.empty()) { + annotations_object.assign_assume_new( + std::string{"title"}, sourcemeta::core::JSON{annotations.title}, + MCP_HASH_TITLE); } + annotations_object.assign_assume_new( + std::string{"readOnlyHint"}, + sourcemeta::core::JSON{annotations.read_only}, MCP_HASH_READ_ONLY_HINT); + annotations_object.assign_assume_new( + std::string{"destructiveHint"}, + sourcemeta::core::JSON{annotations.destructive}, + MCP_HASH_DESTRUCTIVE_HINT); + annotations_object.assign_assume_new( + std::string{"idempotentHint"}, + sourcemeta::core::JSON{annotations.idempotent}, MCP_HASH_IDEMPOTENT_HINT); + annotations_object.assign_assume_new( + std::string{"openWorldHint"}, + sourcemeta::core::JSON{annotations.open_world}, MCP_HASH_OPEN_WORLD_HINT); + entry.assign_assume_new(std::string{"annotations"}, + std::move(annotations_object), MCP_HASH_ANNOTATIONS); + return entry; } auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, - sourcemeta::core::JSON capabilities, - sourcemeta::core::JSON server_info, + const MCPServerCapabilities &capabilities, + const MCPImplementation &server, const std::string_view instructions) -> sourcemeta::core::JSON { const auto *id{sourcemeta::core::jsonrpc_request_id(request)}; @@ -192,28 +306,79 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, } std::string_view requested_version{}; - if (params->defines("protocolVersion") && - params->at("protocolVersion").is_string()) { - requested_version = params->at("protocolVersion").to_string(); + if (params->defines("protocolVersion", MCP_HASH_PROTOCOL_VERSION) && + params->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION).is_string()) { + requested_version = + params->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION).to_string(); } const auto resolved{mcp_resolve_protocol_version(requested_version)}; const auto version{resolved.value_or(MCPProtocolVersion::V_2025_11_25)}; - if (!mcp_supports_implementation_title(version) && server_info.is_object() && - server_info.defines("title")) { - server_info.erase("title"); + auto capabilities_object{sourcemeta::core::JSON::make_object()}; + if (capabilities.prompts) { + capabilities_object.assign_assume_new(std::string{"prompts"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_PROMPTS); + } + if (capabilities.resources) { + capabilities_object.assign_assume_new(std::string{"resources"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_RESOURCES); + } + if (capabilities.tools) { + capabilities_object.assign_assume_new(std::string{"tools"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_TOOLS); + } + if (capabilities.logging) { + capabilities_object.assign_assume_new(std::string{"logging"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_LOGGING); + } + if (capabilities.completions) { + capabilities_object.assign_assume_new(std::string{"completions"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_COMPLETIONS); + } + + auto server_info{sourcemeta::core::JSON::make_object()}; + server_info.assign_assume_new( + std::string{"name"}, sourcemeta::core::JSON{server.name}, MCP_HASH_NAME); + server_info.assign_assume_new(std::string{"version"}, + sourcemeta::core::JSON{server.version}, + MCP_HASH_VERSION); + if (!server.title.empty() && mcp_supports_implementation_title(version)) { + server_info.assign_assume_new(std::string{"title"}, + sourcemeta::core::JSON{server.title}, + MCP_HASH_TITLE); + } + if (!server.description.empty() && + mcp_supports_implementation_description(version)) { + server_info.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{server.description}, + MCP_HASH_DESCRIPTION); + } + if (!server.website_url.empty() && + mcp_supports_implementation_website_url(version)) { + server_info.assign_assume_new(std::string{"websiteUrl"}, + sourcemeta::core::JSON{server.website_url}, + MCP_HASH_WEBSITE_URL); } auto result{sourcemeta::core::JSON::make_object()}; result.assign_assume_new( std::string{"protocolVersion"}, - sourcemeta::core::JSON{mcp_protocol_version_string(version)}); + sourcemeta::core::JSON{mcp_protocol_version_string(version)}, + MCP_HASH_PROTOCOL_VERSION); result.assign_assume_new(std::string{"capabilities"}, - std::move(capabilities)); - result.assign_assume_new(std::string{"serverInfo"}, std::move(server_info)); + std::move(capabilities_object), + MCP_HASH_CAPABILITIES); + result.assign_assume_new(std::string{"serverInfo"}, std::move(server_info), + MCP_HASH_SERVER_INFO); if (!instructions.empty()) { result.assign_assume_new(std::string{"instructions"}, - sourcemeta::core::JSON{instructions}); + sourcemeta::core::JSON{instructions}, + MCP_HASH_INSTRUCTIONS); } return sourcemeta::core::jsonrpc_make_success(*id, std::move(result)); } @@ -223,45 +388,32 @@ auto mcp_make_resource_text_content(const std::string_view uri, const std::string_view text) -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; - entry.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}); + entry.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); entry.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}); - entry.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}); + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + entry.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, + MCP_HASH_TEXT); return entry; } auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) -> sourcemeta::core::JSON { auto result{sourcemeta::core::JSON::make_object()}; - result.assign_assume_new(std::string{"contents"}, std::move(contents)); + result.assign_assume_new(std::string{"contents"}, std::move(contents), + MCP_HASH_CONTENTS); return result; } -auto mcp_parse_cursor_as_unsigned_integer(const std::string_view cursor) - -> std::optional { - if (cursor.empty()) { - return std::nullopt; - } - if (cursor.size() > 1 && cursor.front() == '0') { - return std::nullopt; - } - std::size_t parsed{0}; - const auto [end, error]{ - std::from_chars(cursor.data(), cursor.data() + cursor.size(), parsed)}; - if (error != std::errc{} || end != cursor.data() + cursor.size()) { - return std::nullopt; - } - return parsed; -} - auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON * { const auto *params{sourcemeta::core::jsonrpc_params(envelope)}; if (params == nullptr || !params->is_object() || - !params->defines("arguments")) { + !params->defines("arguments", MCP_HASH_ARGUMENTS)) { return nullptr; } - return ¶ms->at("arguments"); + return ¶ms->at("arguments", MCP_HASH_ARGUMENTS); } } // namespace sourcemeta::one diff --git a/src/router/include/sourcemeta/one/router.h b/src/router/include/sourcemeta/one/router.h index f8d469ab..4c322f22 100644 --- a/src/router/include/sourcemeta/one/router.h +++ b/src/router/include/sourcemeta/one/router.h @@ -105,8 +105,7 @@ class Router { public: Router(const std::filesystem::path &base, const core::URITemplateRouterView &router, - std::span constructors, - std::span descriptions); + std::span constructors); ~Router() = default; // To avoid mistakes @@ -132,10 +131,6 @@ class Router { const char *const code, std::string &&identifier, std::string &&message) const -> void; - [[nodiscard]] auto - description(core::URITemplateRouter::Identifier context) const noexcept - -> std::string_view; - private: struct Slot { std::unique_ptr instance; @@ -147,7 +142,6 @@ class Router { // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) const core::URITemplateRouterView &router_; std::span constructors_; - std::span descriptions_; // NOLINTNEXTLINE(modernize-avoid-c-arrays) std::unique_ptr slots_; std::size_t slots_size_; diff --git a/src/router/router.cc b/src/router/router.cc index 77537432..9a2f2dc2 100644 --- a/src/router/router.cc +++ b/src/router/router.cc @@ -9,10 +9,8 @@ namespace sourcemeta::one { Router::Router(const std::filesystem::path &base, const sourcemeta::core::URITemplateRouterView &router, - const std::span constructors, - const std::span descriptions) + const std::span constructors) : base_{base}, router_{router}, constructors_{constructors}, - descriptions_{descriptions}, // NOLINTNEXTLINE(modernize-avoid-c-arrays) slots_{std::make_unique(router.size() + 1)}, slots_size_{router.size() + 1} { @@ -23,14 +21,6 @@ Router::Router(const std::filesystem::path &base, }); } -auto Router::description(const sourcemeta::core::URITemplateRouter::Identifier - context) const noexcept -> std::string_view { - if (context >= this->descriptions_.size()) { - return {}; - } - return this->descriptions_[context]; -} - auto Router::error(const sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response, const char *const code, std::string &&identifier, diff --git a/src/self/v1/schemas/mcp/response.json b/src/self/v1/schemas/mcp/response.json index 3d57c9bc..e204dcc5 100644 --- a/src/self/v1/schemas/mcp/response.json +++ b/src/self/v1/schemas/mcp/response.json @@ -80,7 +80,11 @@ "$ref": "https://example.com/v1/self/v1/schemas/api/list/response" }, "annotations": { - "title": "Tool title" + "title": "Tool title", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false } } ] diff --git a/src/self/v1/schemas/mcp/tools/list/response.json b/src/self/v1/schemas/mcp/tools/list/response.json index 210b1ed2..4ac7c618 100644 --- a/src/self/v1/schemas/mcp/tools/list/response.json +++ b/src/self/v1/schemas/mcp/tools/list/response.json @@ -18,7 +18,11 @@ "$ref": "https://example.com/v1/self/v1/schemas/api/list/response" }, "annotations": { - "title": "Tool title" + "title": "Tool title", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false } } ] @@ -81,11 +85,29 @@ }, "annotations": { "type": "object", - "required": [ "title" ], + "required": [ + "title", + "readOnlyHint", + "destructiveHint", + "idempotentHint", + "openWorldHint" + ], "properties": { "title": { "type": "string", "minLength": 1 + }, + "readOnlyHint": { + "type": "boolean" + }, + "destructiveHint": { + "type": "boolean" + }, + "idempotentHint": { + "type": "boolean" + }, + "openWorldHint": { + "type": "boolean" } }, "additionalProperties": false diff --git a/src/server/server.cc b/src/server/server.cc index d7b1f683..81859dc6 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -94,8 +94,8 @@ auto main(int argc, char *argv[]) noexcept -> int { } const sourcemeta::core::URITemplateRouterView router{base / "routes.bin"}; - sourcemeta::one::Router actions{base, router, sourcemeta::one::CONSTRUCTORS, - sourcemeta::one::DESCRIPTIONS}; + sourcemeta::one::Router actions{base, router, + sourcemeta::one::CONSTRUCTORS}; sourcemeta::one::HTTPServer( port,