Skip to content

Conversation

@shamashel
Copy link

@shamashel shamashel commented Dec 29, 2025

Closes #2308

Adds support for defining custom MCP tool names on GraphQL operations using the @mcpTool(name: "...") directive:

query GetEmployee($id: Int!) @mcpTool(name: "get_employee_by_id") {
  employee(id: $id) { ... }
}

Without the directive, tools continue using the default execute_operation_<snake_case_name> naming convention.

Checklist

Summary by CodeRabbit

  • New Features

    • Operations can declare custom MCP tool names via a directive; declared names override generated identifiers and are reflected in discovery/listing.
  • Tests

    • Added tests covering directive parsing, operation loading, discovery/listing flows, and per-tool validation for custom and default names.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

Adds per-operation MCP tool naming via a new @mcpTool(name: "...") directive: the schema loader extracts ToolName, strips the directive before validation, stores ToolName on Operation, and the MCP server registers tools using the custom name with fallback to the generated execute_operation_<snake_case> name. Tests and fixtures added.

Changes

Cohort / File(s) Summary
Tests — discovery & validation
router-tests/mcp_test.go
Replace broad discovery assertions with explicit lookups; add tests verifying a custom tool name (get_employee_by_id) is registered and the default generated name is not when a custom name is supplied.
Test data — new GraphQL fixture
router-tests/testdata/mcp_operations/CustomToolName.graphql
New GraphQL file defining CustomNamedQuery annotated with @mcpTool(name: "get_employee_by_id") to exercise custom tool naming.
Schema loader — implementation
router/pkg/schemaloader/loader.go
Add ToolName string to Operation; extract MCP tool name with extractMCPToolName, strip @mcpTool via stripMCPDirective before validation, compute description/toolName pre-validation and set Operation.ToolName.
Schema loader — tests
router/pkg/schemaloader/loader_test.go
Add TestExtractMCPToolName and TestLoadOperationsWithMCPDirective to validate directive extraction, stripping, and propagation of ToolName into loaded operations.
MCP server — registration
router/pkg/mcpserver/server.go
registerTools now uses op.ToolName when present; otherwise generates default execute_operation_<snake_case> tool name.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(router): allow custom MCP tool names via @mcptool directive' clearly and concisely describes the main change: enabling users to define custom MCP tool names using a GraphQL directive.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #2308 by adding support for custom MCP tool names via @mcptool directive with fallback to default naming convention.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of allowing custom MCP tool names; no extraneous modifications detected outside the stated requirement scope.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57714a4 and 08ea5bd.

📒 Files selected for processing (1)
  • router-tests/mcp_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-01T20:39:16.113Z
Learnt from: SkArchon
Repo: wundergraph/cosmo PR: 2252
File: router-tests/telemetry/telemetry_test.go:9684-9693
Timestamp: 2025-10-01T20:39:16.113Z
Learning: Repo preference: In router-tests/telemetry/telemetry_test.go, keep strict > 0 assertions for request.operation.*Time (parsingTime, normalizationTime, validationTime, planningTime) in telemetry-related tests; do not relax to >= 0 unless CI flakiness is observed.

Applied to files:

  • router-tests/mcp_test.go
📚 Learning: 2025-08-20T22:13:25.222Z
Learnt from: StarpTech
Repo: wundergraph/cosmo PR: 2157
File: router-tests/go.mod:16-16
Timestamp: 2025-08-20T22:13:25.222Z
Learning: github.com/mark3labs/mcp-go v0.38.0 has regressions and should not be used in the wundergraph/cosmo project. v0.36.0 is the stable version that should be used across router-tests and other modules.

Applied to files:

  • router-tests/mcp_test.go
🧬 Code graph analysis (1)
router-tests/mcp_test.go (2)
router-tests/testenv/testenv.go (3)
  • Run (105-122)
  • Config (292-349)
  • Environment (1735-1771)
router/pkg/config/config.go (2)
  • Config (1031-1106)
  • MCPConfiguration (991-1001)
🔇 Additional comments (3)
router-tests/mcp_test.go (3)

45-54: LGTM! Schema update is consistent with the new operation.

The addition of "CustomNamedQuery" to the enum and the reformatted schema structure align correctly with the new custom-named operation being introduced.


13-13: The mcp-go version in router-tests/go.mod is v0.36.0 (stable), so no action is required.


197-225: Add an execution test for the custom-named tool to complete test coverage.

The discovery test correctly verifies that the custom tool name get_employee_by_id is registered via the @mcpTool directive. However, the test pattern in this file shows other operations include corresponding execution tests. The CustomNamedQuery operation requires an id parameter and should have a similar execution test to verify it can be invoked successfully with the custom name.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
router/pkg/schemaloader/loader.go (1)

258-270: Consider using the constant for consistency.

Line 265 uses the hardcoded string "mcpTool" while line 205 defines mcpDirectiveName as a byte slice. For consistency and maintainability, consider using the constant.

🔎 Proposed fix
 func stripMCPDirective(doc *ast.Document) {
 	for _, ref := range doc.RootNodes {
 		if ref.Kind == ast.NodeKindOperationDefinition {
 			opDef := &doc.OperationDefinitions[ref.Ref]
 			if !opDef.HasDirectives {
 				return
 			}
-			opDef.Directives.RemoveDirectiveByName(doc, "mcpTool")
+			opDef.Directives.RemoveDirectiveByName(doc, string(mcpDirectiveName))
 			opDef.HasDirectives = len(opDef.Directives.Refs) > 0
 			return
 		}
 	}
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98bb8e9 and 82a3d2c.

📒 Files selected for processing (5)
  • router-tests/mcp_test.go
  • router-tests/testdata/mcp_operations/CustomToolName.graphql
  • router/pkg/mcpserver/server.go
  • router/pkg/schemaloader/loader.go
  • router/pkg/schemaloader/loader_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-01T20:39:16.113Z
Learnt from: SkArchon
Repo: wundergraph/cosmo PR: 2252
File: router-tests/telemetry/telemetry_test.go:9684-9693
Timestamp: 2025-10-01T20:39:16.113Z
Learning: Repo preference: In router-tests/telemetry/telemetry_test.go, keep strict > 0 assertions for request.operation.*Time (parsingTime, normalizationTime, validationTime, planningTime) in telemetry-related tests; do not relax to >= 0 unless CI flakiness is observed.

Applied to files:

  • router-tests/mcp_test.go
📚 Learning: 2025-08-20T22:13:25.222Z
Learnt from: StarpTech
Repo: wundergraph/cosmo PR: 2157
File: router-tests/go.mod:16-16
Timestamp: 2025-08-20T22:13:25.222Z
Learning: github.com/mark3labs/mcp-go v0.38.0 has regressions and should not be used in the wundergraph/cosmo project. v0.36.0 is the stable version that should be used across router-tests and other modules.

Applied to files:

  • router-tests/mcp_test.go
🧬 Code graph analysis (1)
router/pkg/schemaloader/loader_test.go (1)
router/pkg/schemaloader/loader.go (2)
  • NewOperationLoader (39-44)
  • Operation (19-28)
🔇 Additional comments (9)
router-tests/testdata/mcp_operations/CustomToolName.graphql (1)

1-11: LGTM!

Clean test fixture demonstrating the custom MCP tool name feature. The query correctly uses the @mcpTool(name: "get_employee_by_id") directive and will be used by integration tests to verify tool discovery and naming.

router/pkg/mcpserver/server.go (1)

539-546: LGTM!

The tool name resolution logic correctly prioritizes the custom op.ToolName from the @mcpTool directive, with an appropriate fallback to the default naming convention (execute_operation_<snake_case_name>).

router/pkg/schemaloader/loader_test.go (2)

169-211: LGTM!

Well-structured table-driven tests covering key scenarios:

  • Directive with name argument
  • No directive
  • Directive without name argument
  • Different directive
  • Directive with other arguments but no name

Good edge case coverage for the extractMCPToolName function.


213-271: LGTM!

Good integration test that validates the complete loading flow with MCP directives. The test correctly verifies that:

  • Operations with @mcpTool(name: "...") have ToolName set
  • Operations without the directive have an empty ToolName
router-tests/mcp_test.go (2)

41-55: LGTM!

The refactored assertion properly validates the get_operation_info tool by:

  1. Finding it by name in the tools list
  2. Verifying the description
  3. Checking the operationName enum contains all three expected operations including the new CustomNamedQuery

192-220: LGTM!

Excellent test coverage for the custom tool name feature:

  • Verifies the custom-named tool get_employee_by_id exists and has the correct description
  • Negative assertion ensures the default-generated name execute_operation_custom_named_query is not registered, confirming the custom name takes precedence
router/pkg/schemaloader/loader.go (3)

208-228: LGTM!

The custom validator correctly omits KnownDirectives validation to allow the @mcpTool directive without requiring it to be defined in the schema. All other standard validation rules are appropriately included.


230-256: LGTM!

The extractMCPToolName function correctly handles all edge cases:

  • Returns empty string when no directives exist
  • Returns empty string when @mcpTool directive is not found
  • Returns empty string when name argument is missing
  • Returns empty string for non-string argument values
  • Only processes the first operation definition (appropriate given the single-operation-per-file constraint)

95-97: LGTM!

Correct ordering: extracting the description and tool name before stripping the directive ensures the directive data is available for extraction, then the directive is removed before validation.

@shamashel
Copy link
Author

First contribution, let me know if there's a process I missed

@shamashel
Copy link
Author

@coderabbitai generate docstrings

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #2434

coderabbitai bot added a commit that referenced this pull request Dec 29, 2025
Docstrings generation was requested by @shamashel.

* #2433 (comment)

The following files were modified:

* `router/pkg/schemaloader/loader.go`
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
router/pkg/schemaloader/loader.go (1)

258-270: Consider using the constant for directive name consistency.

Line 265 uses the string literal "mcpTool" directly, while extractMCPToolName uses the mcpDirectiveName constant. Using the constant consistently would prevent potential drift if the directive name changes.

🔎 Proposed fix
 func stripMCPDirective(doc *ast.Document) {
 	for _, ref := range doc.RootNodes {
 		if ref.Kind == ast.NodeKindOperationDefinition {
 			opDef := &doc.OperationDefinitions[ref.Ref]
 			if !opDef.HasDirectives {
 				return
 			}
-			opDef.Directives.RemoveDirectiveByName(doc, "mcpTool")
+			opDef.Directives.RemoveDirectiveByName(doc, string(mcpDirectiveName))
 			opDef.HasDirectives = len(opDef.Directives.Refs) > 0
 			return
 		}
 	}
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82a3d2c and d5c4cda.

📒 Files selected for processing (1)
  • router/pkg/schemaloader/loader.go
🔇 Additional comments (5)
router/pkg/schemaloader/loader.go (5)

18-28: LGTM on struct extension.

The new ToolName field correctly extends the Operation struct to carry the custom MCP tool name. Consider adding a brief doc comment to clarify its purpose and relationship to the @mcpTool directive.


95-100: Good ordering of extraction and validation.

The flow correctly extracts the tool name and description before stripping the directive, then validates the cleaned document. This ensures the directive doesn't cause validation failures while still capturing its data.


208-228: Custom validator correctly excludes KnownDirectives().

The validator intentionally omits KnownDirectives() to allow the @mcpTool directive which isn't defined in the GraphQL schema. All other standard validation rules are appropriately included.


204-206: LGTM on directive constants.

Using package-level var for byte slices is the correct Go idiom since const doesn't support slice types.


230-256: Fallback handling for empty tool names is correctly implemented.

The function extracts the tool name correctly, and downstream code at router/pkg/mcpserver/server.go (lines 541–546) properly falls back to default execute_operation_<snake_case> naming when ToolName is empty or missing.

The custom newMCPOperationValidator was unnecessary because
stripMCPDirective removes the @mcptool directive before validation runs.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
router/pkg/schemaloader/loader.go (2)

205-206: Consider using string constants instead of byte slice variables.

While the current approach works, declaring these as const strings and converting to []byte at call sites would make them truly immutable and align with Go best practices for constant values.

🔎 Optional refactoring
-var mcpDirectiveName = []byte("mcpTool")
-var mcpNameArgument = []byte("name")
+const mcpDirectiveName = "mcpTool"
+const mcpNameArgument = "name"

Then update call sites to convert when needed:

-			directiveRef, exists := doc.DirectiveWithNameBytes(opDef.Directives.Refs, mcpDirectiveName)
+			directiveRef, exists := doc.DirectiveWithNameBytes(opDef.Directives.Refs, []byte(mcpDirectiveName))

And:

-			opDef.Directives.RemoveDirectiveByName(doc, string(mcpDirectiveName))
+			opDef.Directives.RemoveDirectiveByName(doc, mcpDirectiveName)

209-235: Consider logging a warning for invalid @mcptool directive arguments.

The function silently returns an empty string when the directive exists but has a non-string argument (line 227-231). While the fallback to default naming is safe, a warning log would help developers identify misconfigured directives during development.

🔎 Optional enhancement
 		value, argExists := doc.DirectiveArgumentValueByName(directiveRef, mcpNameArgument)
 		if !argExists {
 			return ""
 		}
 
 		if value.Kind == ast.ValueKindString {
 			return doc.StringValueContentString(value.Ref)
 		}
+		// Log warning: directive exists but argument is not a string
+		// (would require passing logger to this function)
 
 		return ""

Note: This would require passing the logger to extractMCPToolName, which may not be worth the added complexity for this edge case.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2abf4d and 57714a4.

📒 Files selected for processing (1)
  • router/pkg/schemaloader/loader.go
🔇 Additional comments (2)
router/pkg/schemaloader/loader.go (2)

21-21: LGTM! Clean implementation of custom tool name extraction.

The new ToolName field and its extraction flow are well-structured. Extracting the tool name and description before stripping the directive ensures the metadata is preserved, and the pre-validation ordering allows the default validator to be used without modification.

Also applies to: 95-97, 125-125, 130-130


238-250: LGTM! Directive stripping logic is sound.

The function correctly removes the @mcpTool directive before validation and updates the HasDirectives flag appropriately. The early returns are safe given the single-operation constraint enforced by parseOperation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow users to define the MCP tool's name

1 participant