Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions router-tests/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,101 @@ func TestMCP(t *testing.T) {
})
})

t.Run("List user Operations / Tool names omit prefix when OmitToolNamePrefix is enabled", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
MCP: config.MCPConfiguration{
Enabled: true,
OmitToolNamePrefix: true,
},
}, func(t *testing.T, xEnv *testenv.Environment) {

toolsRequest := mcp.ListToolsRequest{}
resp, err := xEnv.MCPClient.ListTools(xEnv.Context, toolsRequest)
require.NoError(t, err)
require.NotNil(t, resp)

var foundMyEmployees, foundUpdateMood bool
var foundPrefixedMyEmployees, foundPrefixedUpdateMood bool

for _, tool := range resp.Tools {
switch tool.Name {
case "my_employees":
foundMyEmployees = true
case "update_mood":
foundUpdateMood = true
case "execute_operation_my_employees":
foundPrefixedMyEmployees = true
case "execute_operation_update_mood":
foundPrefixedUpdateMood = true
}
}

require.True(t, foundMyEmployees, "Tool 'my_employees' should be registered when OmitToolNamePrefix is true")
require.True(t, foundUpdateMood, "Tool 'update_mood' should be registered when OmitToolNamePrefix is true")

require.False(t, foundPrefixedMyEmployees, "Tool 'execute_operation_my_employees' should NOT be registered when OmitToolNamePrefix is true")
require.False(t, foundPrefixedUpdateMood, "Tool 'execute_operation_update_mood' should NOT be registered when OmitToolNamePrefix is true")
})
})

t.Run("Execute operation using short tool name when OmitToolNamePrefix is enabled", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
MCP: config.MCPConfiguration{
Enabled: true,
OmitToolNamePrefix: true,
},
}, func(t *testing.T, xEnv *testenv.Environment) {

req := mcp.CallToolRequest{}
req.Params.Name = "my_employees"
req.Params.Arguments = map[string]interface{}{
"criteria": map[string]interface{}{},
}

resp, err := xEnv.MCPClient.CallTool(xEnv.Context, req)
assert.NoError(t, err)
assert.NotNil(t, resp)

assert.Len(t, resp.Content, 1)

content, ok := resp.Content[0].(mcp.TextContent)
assert.True(t, ok)

assert.Equal(t, content.Type, "text")
assert.Contains(t, content.Text, "findEmployees")
})
})

t.Run("Tool name collision with built-in tool uses prefixed name when OmitToolNamePrefix is enabled", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
MCPOperationsPath: "testdata/mcp_operations_collision",
MCP: config.MCPConfiguration{
Enabled: true,
OmitToolNamePrefix: true,
ExposeSchema: true,
},
}, func(t *testing.T, xEnv *testenv.Environment) {
toolsRequest := mcp.ListToolsRequest{}
resp, err := xEnv.MCPClient.ListTools(xEnv.Context, toolsRequest)
require.NoError(t, err)
require.NotNil(t, resp)

var foundBuiltInGetSchema, foundPrefixedGetSchema bool

for _, tool := range resp.Tools {
switch tool.Name {
case "get_schema":
foundBuiltInGetSchema = true
case "execute_operation_get_schema":
foundPrefixedGetSchema = true
}
}

require.True(t, foundBuiltInGetSchema, "Built-in 'get_schema' tool should be registered")
require.True(t, foundPrefixedGetSchema, "Conflicting operation should use prefixed name 'execute_operation_get_schema'")
})
})

t.Run("List user Operations / Static operations of type mutation aren't exposed when excludeMutations is set", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
MCP: config.MCPConfiguration{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query GetSchema {
employees {
id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mutation UpdateMood($employeeID: Int! $mood: Mood!) {
updateMood(employeeID: $employeeID mood: $mood) {
id
details {
forename
}
currentMood
}
}
12 changes: 12 additions & 0 deletions router-tests/testdata/mcp_operations_collision/MyQuery.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query MyEmployees($criteria: SearchInput) {
findEmployees(criteria: $criteria) {
id
isAvailable
currentMood
products
details {
forename
nationality
}
}
}
8 changes: 6 additions & 2 deletions router-tests/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ type Config struct {
UseVersionedGraph bool
NoShutdownTestServer bool
MCP config.MCPConfiguration
MCPOperationsPath string
EnableRedis bool
EnableRedisCluster bool
Plugins PluginConfig
Expand Down Expand Up @@ -1438,12 +1439,15 @@ func configureRouter(listenerAddr string, testConfig *Config, routerConfig *node
}

if testConfig.MCP.Enabled {
// Add Storage provider
mcpOperationsPath := "testdata/mcp_operations"
if testConfig.MCPOperationsPath != "" {
mcpOperationsPath = testConfig.MCPOperationsPath
}
routerOpts = append(routerOpts, core.WithStorageProviders(config.StorageProviders{
FileSystem: []config.FileSystemStorageProvider{
{
ID: "test",
Path: "testdata/mcp_operations",
Path: mcpOperationsPath,
},
},
}))
Expand Down
1 change: 1 addition & 0 deletions router/core/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ func (r *Router) bootstrap(ctx context.Context) error {
mcpserver.WithExcludeMutations(r.mcp.ExcludeMutations),
mcpserver.WithEnableArbitraryOperations(r.mcp.EnableArbitraryOperations),
mcpserver.WithExposeSchema(r.mcp.ExposeSchema),
mcpserver.WithOmitToolNamePrefix(r.mcp.OmitToolNamePrefix),
mcpserver.WithStateless(r.mcp.Session.Stateless),
}

Expand Down
37 changes: 20 additions & 17 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,25 +360,25 @@ func (r *ResponseHeaderRule) GetMatching() string {
}

type EngineDebugConfiguration struct {
PrintOperationTransformations bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_OPERATION_TRANSFORMATIONS" yaml:"print_operation_transformations"`
PrintOperationEnableASTRefs bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_OPERATION_ENABLE_AST_REFS" yaml:"print_operation_enable_ast_refs"`
PrintPlanningPaths bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_PLANNING_PATHS" yaml:"print_planning_paths"`
PrintQueryPlans bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_QUERY_PLANS" yaml:"print_query_plans"`
PrintIntermediateQueryPlans bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_INTERMEDIATE_QUERY_PLANS" yaml:"print_intermediate_query_plans"`
PrintNodeSuggestions bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_NODE_SUGGESTIONS" yaml:"print_node_suggestions"`
ConfigurationVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_CONFIGURATION_VISITOR" yaml:"configuration_visitor"`
PlanningVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_PLANNING_VISITOR" yaml:"planning_visitor"`
DatasourceVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_DATASOURCE_VISITOR" yaml:"datasource_visitor"`
ReportWebSocketConnections bool `envDefault:"false" env:"ENGINE_DEBUG_REPORT_WEBSOCKET_CONNECTIONS" yaml:"report_websocket_connections"`
ReportMemoryUsage bool `envDefault:"false" env:"ENGINE_DEBUG_REPORT_MEMORY_USAGE" yaml:"report_memory_usage"`
EnableResolverDebugging bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_RESOLVER_DEBUGGING" yaml:"enable_resolver_debugging"`
PrintOperationTransformations bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_OPERATION_TRANSFORMATIONS" yaml:"print_operation_transformations"`
PrintOperationEnableASTRefs bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_OPERATION_ENABLE_AST_REFS" yaml:"print_operation_enable_ast_refs"`
PrintPlanningPaths bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_PLANNING_PATHS" yaml:"print_planning_paths"`
PrintQueryPlans bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_QUERY_PLANS" yaml:"print_query_plans"`
PrintIntermediateQueryPlans bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_INTERMEDIATE_QUERY_PLANS" yaml:"print_intermediate_query_plans"`
PrintNodeSuggestions bool `envDefault:"false" env:"ENGINE_DEBUG_PRINT_NODE_SUGGESTIONS" yaml:"print_node_suggestions"`
ConfigurationVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_CONFIGURATION_VISITOR" yaml:"configuration_visitor"`
PlanningVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_PLANNING_VISITOR" yaml:"planning_visitor"`
DatasourceVisitor bool `envDefault:"false" env:"ENGINE_DEBUG_DATASOURCE_VISITOR" yaml:"datasource_visitor"`
ReportWebSocketConnections bool `envDefault:"false" env:"ENGINE_DEBUG_REPORT_WEBSOCKET_CONNECTIONS" yaml:"report_websocket_connections"`
ReportMemoryUsage bool `envDefault:"false" env:"ENGINE_DEBUG_REPORT_MEMORY_USAGE" yaml:"report_memory_usage"`
EnableResolverDebugging bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_RESOLVER_DEBUGGING" yaml:"enable_resolver_debugging"`
// EnablePersistedOperationsCacheResponseHeader is deprecated, use EnableCacheResponseHeaders instead.
EnablePersistedOperationsCacheResponseHeader bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_PERSISTED_OPERATIONS_CACHE_RESPONSE_HEADER" yaml:"enable_persisted_operations_cache_response_header"`
// EnableNormalizationCacheResponseHeader is deprecated, use EnableCacheResponseHeaders instead.
EnableNormalizationCacheResponseHeader bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_NORMALIZATION_CACHE_RESPONSE_HEADER" yaml:"enable_normalization_cache_response_header"`
EnableCacheResponseHeaders bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_CACHE_RESPONSE_HEADERS" yaml:"enable_cache_response_headers"`
AlwaysIncludeQueryPlan bool `envDefault:"false" env:"ENGINE_DEBUG_ALWAYS_INCLUDE_QUERY_PLAN" yaml:"always_include_query_plan"`
AlwaysSkipLoader bool `envDefault:"false" env:"ENGINE_DEBUG_ALWAYS_SKIP_LOADER" yaml:"always_skip_loader"`
EnableNormalizationCacheResponseHeader bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_NORMALIZATION_CACHE_RESPONSE_HEADER" yaml:"enable_normalization_cache_response_header"`
EnableCacheResponseHeaders bool `envDefault:"false" env:"ENGINE_DEBUG_ENABLE_CACHE_RESPONSE_HEADERS" yaml:"enable_cache_response_headers"`
AlwaysIncludeQueryPlan bool `envDefault:"false" env:"ENGINE_DEBUG_ALWAYS_INCLUDE_QUERY_PLAN" yaml:"always_include_query_plan"`
AlwaysSkipLoader bool `envDefault:"false" env:"ENGINE_DEBUG_ALWAYS_SKIP_LOADER" yaml:"always_skip_loader"`
}

type EngineExecutionConfiguration struct {
Expand Down Expand Up @@ -997,7 +997,10 @@ type MCPConfiguration struct {
ExcludeMutations bool `yaml:"exclude_mutations" envDefault:"false" env:"MCP_EXCLUDE_MUTATIONS"`
EnableArbitraryOperations bool `yaml:"enable_arbitrary_operations" envDefault:"false" env:"MCP_ENABLE_ARBITRARY_OPERATIONS"`
ExposeSchema bool `yaml:"expose_schema" envDefault:"false" env:"MCP_EXPOSE_SCHEMA"`
RouterURL string `yaml:"router_url,omitempty" env:"MCP_ROUTER_URL"`
// OmitToolNamePrefix removes the "execute_operation_" prefix from MCP tool names.
// When enabled, GetUser becomes get_user. When disabled (default), GetUser becomes execute_operation_get_user.
OmitToolNamePrefix bool `yaml:"omit_tool_name_prefix" envDefault:"false" env:"MCP_OMIT_TOOL_NAME_PREFIX"`
RouterURL string `yaml:"router_url,omitempty" env:"MCP_ROUTER_URL"`
}

type MCPSessionConfig struct {
Expand Down
5 changes: 5 additions & 0 deletions router/pkg/config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,11 @@
"type": "boolean",
"default": false,
"description": "Expose the full GraphQL schema through MCP. When enabled, AI models can request the complete schema of your API."
},
"omit_tool_name_prefix": {
"type": "boolean",
"default": false,
"description": "When enabled, MCP tool names generated from GraphQL operations omit the 'execute_operation_' prefix. For example, the GraphQL operation 'GetUser' results in a tool named 'get_user' instead of 'execute_operation_get_user'. This produces cleaner tool names for AI models."
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion router/pkg/config/fixtures/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mcp:
expose_schema: false
enable_arbitrary_operations: false
exclude_mutations: false
omit_tool_name_prefix: false
graph_name: cosmo
router_url: https://cosmo-router.wundergraph.com
server:
Expand Down Expand Up @@ -177,7 +178,7 @@ telemetry:
schema_usage:
enabled: true
include_operation_sha: true
sample_rate: 1.0 # Supports any rate: 1.0, 0.8, 0.5, 0.1, 0.01, etc.
sample_rate: 1.0 # Supports any rate: 1.0, 0.8, 0.5, 0.1, 0.01, etc.

cache_control_policy:
enabled: true
Expand Down
35 changes: 9 additions & 26 deletions router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,9 @@
},
"CORS": {
"Enabled": true,
"AllowOrigins": [
"*"
],
"AllowMethods": [
"HEAD",
"GET",
"POST"
],
"AllowHeaders": [
"Origin",
"Content-Length",
"Content-Type"
],
"AllowOrigins": ["*"],
"AllowMethods": ["HEAD", "GET", "POST"],
"AllowHeaders": ["Origin", "Content-Length", "Content-Type"],
"AllowCredentials": true,
"MaxAge": 300000000000
},
Expand Down Expand Up @@ -137,6 +127,7 @@
"ExcludeMutations": false,
"EnableArbitraryOperations": false,
"ExposeSchema": false,
"OmitToolNamePrefix": false,
"RouterURL": ""
},
"DemoMode": false,
Expand Down Expand Up @@ -213,9 +204,7 @@
},
"Router": {
"Fields": null,
"IgnoreQueryParamsList": [
"variables"
]
"IgnoreQueryParamsList": ["variables"]
},
"Subgraphs": {
"Enabled": false,
Expand Down Expand Up @@ -408,15 +397,11 @@
},
"ForwardUpgradeHeaders": {
"Enabled": true,
"AllowList": [
"Authorization"
]
"AllowList": ["Authorization"]
},
"ForwardUpgradeQueryParams": {
"Enabled": true,
"AllowList": [
"Authorization"
]
"AllowList": ["Authorization"]
},
"ForwardInitialPayload": true,
"Authentication": {
Expand Down Expand Up @@ -450,9 +435,7 @@
"AttachServiceName": true,
"DefaultExtensionCode": "DOWNSTREAM_SERVICE_ERROR",
"AllowAllExtensionFields": false,
"AllowedExtensionFields": [
"code"
],
"AllowedExtensionFields": ["code"],
"AllowedFields": null
},
"StorageProviders": {
Expand Down Expand Up @@ -556,4 +539,4 @@
"Maximum": 10000000000
}
}
}
}
Loading
Loading