Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 78 additions & 0 deletions router-tests/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,84 @@ 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)

toolNames := make([]string, len(resp.Tools))
for i, tool := range resp.Tools {
toolNames[i] = tool.Name
}

expectedToolNames := []string{"get_operation_info", "my_employees", "update_mood"}
assert.ElementsMatch(t, expectedToolNames, toolNames)
})
})

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{
Params: mcp.CallToolParams{
Name: "my_employees",
Arguments: map[string]any{
"criteria": map[string]any{},
},
},
}

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)

toolNames := make([]string, len(resp.Tools))
for i, tool := range resp.Tools {
toolNames[i] = tool.Name
}

assert.Contains(t, toolNames, "get_schema") // built-in tool (ExposeSchema=true)
assert.Contains(t, toolNames, "execute_operation_get_schema") // collision uses prefix
})
})

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
3 changes: 3 additions & 0 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,9 @@ type MCPConfiguration struct {
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"`
}

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 @@ -2144,6 +2144,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'."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions 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
3 changes: 2 additions & 1 deletion router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@
"ExcludeMutations": false,
"EnableArbitraryOperations": false,
"ExposeSchema": false,
"RouterURL": ""
"RouterURL": "",
"OmitToolNamePrefix": false
},
"DemoMode": false,
"Modules": null,
Expand Down
3 changes: 2 additions & 1 deletion router/pkg/config/testdata/config_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@
"ExcludeMutations": false,
"EnableArbitraryOperations": false,
"ExposeSchema": false,
"RouterURL": "https://cosmo-router.wundergraph.com"
"RouterURL": "https://cosmo-router.wundergraph.com",
"OmitToolNamePrefix": false
},
"DemoMode": true,
"Modules": {
Expand Down
24 changes: 23 additions & 1 deletion router/pkg/mcpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -90,6 +91,8 @@ type Options struct {
EnableArbitraryOperations bool
// ExposeSchema determines whether the GraphQL schema is exposed
ExposeSchema bool
// OmitToolNamePrefix removes the "execute_operation_" prefix from MCP tool names
OmitToolNamePrefix bool
// Stateless determines whether the MCP server should be stateless
Stateless bool
// CorsConfig is the CORS configuration for the MCP server
Expand All @@ -110,6 +113,7 @@ type GraphQLSchemaServer struct {
excludeMutations bool
enableArbitraryOperations bool
exposeSchema bool
omitToolNamePrefix bool
stateless bool
operationsManager *OperationsManager
schemaCompiler *SchemaCompiler
Expand Down Expand Up @@ -240,6 +244,7 @@ func NewGraphQLSchemaServer(routerGraphQLEndpoint string, opts ...func(*Options)
excludeMutations: options.ExcludeMutations,
enableArbitraryOperations: options.EnableArbitraryOperations,
exposeSchema: options.ExposeSchema,
omitToolNamePrefix: options.OmitToolNamePrefix,
stateless: options.Stateless,
corsConfig: options.CorsConfig,
}
Expand Down Expand Up @@ -307,6 +312,13 @@ func WithStateless(stateless bool) func(*Options) {
}
}

// WithOmitToolNamePrefix sets the omit tool name prefix option
func WithOmitToolNamePrefix(omitToolNamePrefix bool) func(*Options) {
return func(o *Options) {
o.OmitToolNamePrefix = omitToolNamePrefix
}
}

func WithCORS(corsCfg cors.Config) func(*Options) {
return func(o *Options) {
// Force specific CORS settings for MCP server
Expand Down Expand Up @@ -547,7 +559,17 @@ func (s *GraphQLSchemaServer) registerTools() error {
toolDescription = fmt.Sprintf("Executes the GraphQL operation '%s' of type %s.", op.Name, op.OperationType)
}

toolName := fmt.Sprintf("execute_operation_%s", operationToolName)
toolName := operationToolName
if !s.omitToolNamePrefix {
toolName = fmt.Sprintf("execute_operation_%s", operationToolName)
} else if slices.Contains(s.registeredTools, operationToolName) {
s.logger.Warn("Operation name collides with built-in MCP tool, using prefixed name",
zap.String("operation", op.Name),
zap.String("conflicting_tool", operationToolName),
zap.String("using_name", fmt.Sprintf("execute_operation_%s", operationToolName)),
)
toolName = fmt.Sprintf("execute_operation_%s", operationToolName)
}
tool := mcp.NewToolWithRawSchema(
toolName,
toolDescription,
Expand Down
Loading