Summary
The Virtual MCP Server (vMCP) is not preserving the _meta field from backend MCP server responses when forwarding them to clients. According to the MCP specification, _meta is a reserved field that can appear in both requests and responses for protocol-level metadata, but our current implementation only extracts _meta from requests and discards it from responses.
Problem Description
The Model Context Protocol specification defines _meta as a universal metadata field that can appear in:
- Request params (
JSONRPCRequest.params._meta)
- Response results (all
Result types inherit _meta?: { [key: string]: unknown })
- Notifications (
JSONRPCNotification.params._meta)
- Content blocks (
TextContent._meta, ImageContent._meta, etc.)
- Entity definitions (
Tool._meta, Resource._meta, Prompt._meta, etc.)
Current Behavior:
- ✅ vMCP correctly extracts
_meta from incoming request params (pkg/mcp/parser.go:224-230)
- ✅ vMCP makes
_meta available via context (mcp.GetMCPMeta(ctx))
- ❌ vMCP discards
_meta from backend responses when converting them to simplified maps
- ❌ vMCP cannot restore
_meta when constructing response objects for clients
Expected Behavior:
vMCP should preserve _meta fields from backend responses and forward them to clients, enabling:
- Progress token propagation for long-running operations
- Distributed tracing through the full request/response cycle
- Custom metadata from backends reaching clients
- Full MCP specification compliance
Root Cause Analysis
Architecture Flow
Client Request (with _meta)
↓
vMCP Server (preserves request _meta ✅)
↓
Backend MCP Server
↓
Backend Response (with _meta)
↓
vMCP Client Conversion ❌ LOSES _meta HERE
↓
vMCP Server Response (without _meta)
↓
Client receives incomplete response ❌
Code Analysis
1. Backend Response Structure (Correct)
Backends return full MCP response objects with _meta:
// From mark3labs/mcp-go SDK
type CallToolResult struct {
Content []Content // Response content
IsError bool // Error flag
StructuredContent map[string]any // Optional structured output
_meta map[string]unknown // ✅ Metadata present
}
Reference: MCP Spec - CallToolResult schema
2. Client Conversion Layer (Bug Location)
File: pkg/vmcp/client/client.go:391-476
func (h *httpBackendClient) CallTool(
ctx context.Context,
target *vmcp.BackendTarget,
toolName string,
arguments map[string]any,
) (map[string]any, error) {
// ...
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: backendToolName,
Arguments: arguments,
},
})
if err != nil {
return nil, fmt.Errorf("...")
}
// ❌ BUG: Convert result contents to simplified map, discarding _meta
resultMap := make(map[string]any)
if len(result.Content) > 0 {
for i, content := range result.Content {
if textContent, ok := mcp.AsTextContent(content); ok {
resultMap["text"] = textContent.Text
} else if imageContent, ok := mcp.AsImageContent(content); ok {
resultMap["image"] = imageContent.Data
}
// _meta from result is never copied to resultMap
}
}
return resultMap, nil // ← Returns without _meta
}
Problem: The conversion from mcp.CallToolResult to map[string]any extracts only content fields and discards the _meta field entirely.
3. Interface Definition (Design Issue)
File: pkg/vmcp/types.go:247-263
type BackendClient interface {
// CallTool invokes a tool on the backend MCP server.
// Returns the tool output or an error.
CallTool(ctx context.Context, target *BackendTarget, toolName string, arguments map[string]any) (map[string]any, error)
// ReadResource retrieves a resource from the backend MCP server.
// Returns the resource content or an error.
ReadResource(ctx context.Context, target *BackendTarget, uri string) ([]byte, error)
// GetPrompt retrieves a prompt from the backend MCP server.
// Returns the rendered prompt text or an error.
GetPrompt(ctx context.Context, target *BackendTarget, name string, arguments map[string]any) (string, error)
}
Problem: The interface returns simplified types (map[string]any, []byte, string) instead of full MCP response structures that would include _meta.
4. Handler Factory (Cannot Fix Without Upstream Data)
File: pkg/vmcp/server/adapter/handler_factory.go:71-113
func (f *DefaultHandlerFactory) CreateToolHandler(
toolName string,
) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// ...
// Call backend - receives map without _meta
result, err := f.backendClient.CallTool(ctx, target, toolName, args)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// ❌ Cannot restore _meta here - it was already lost upstream
return mcp.NewToolResultStructuredOnly(result), nil
}
}
Problem: By the time we construct the response for the client, _meta has already been discarded in the conversion layer.
Impact Assessment
Affected Operations
All MCP operations that return structured results are affected:
-
Tool Calls (tools/call)
- Missing
_meta in CallToolResult
- Cannot propagate progress tokens, trace IDs, or custom metadata
-
Resource Reads (resources/read)
- Missing
_meta in ReadResourceResult
- Cannot propagate resource-specific metadata
-
Prompt Gets (prompts/get)
- Missing
_meta in GetPromptResult
- Cannot propagate prompt-related metadata
-
List Operations (tools/list, resources/list, prompts/list)
- Missing
_meta in ListToolsResult, ListResourcesResult, ListPromptsResult
- Cannot propagate pagination metadata, cache hints, etc.
-
Capability Discovery (initialize)
- Missing
_meta in InitializeResult
- Cannot propagate server-specific initialization metadata
MCP Specification Compliance
According to the MCP specification (2025-06-18):
The _meta property/parameter is reserved by MCP to allow clients and servers to attach additional metadata to their interactions.
All Result types in the spec inherit the base Result interface:
interface Result {
_meta?: { [key: string]: unknown };
[key: string]: unknown;
}
vMCP is currently non-compliant with this aspect of the specification.
Use Cases Broken
-
Progress Reporting
- Backends cannot report progress via
progressToken in responses
- Clients cannot track long-running operations
-
Distributed Tracing
- Trace context (e.g.,
traceparent) is lost in responses
- End-to-end tracing through vMCP is incomplete
-
Custom Backend Metadata
- Backend-specific metadata (caching hints, rate limits, etc.) is discarded
- Clients cannot make informed decisions based on backend state
-
MCP Tasks (November 2025 Spec)
- Task-related metadata (
io.modelcontextprotocol/related-task) must be preserved
- See: Related Task Metadata
- Current implementation cannot support task association metadata in responses
Affected Code Locations
Core Issue
pkg/vmcp/client/client.go:391-476 - CallTool conversion loses _meta
pkg/vmcp/client/client.go:479-534 - ReadResource conversion loses _meta
pkg/vmcp/client/client.go:535-596 - GetPrompt conversion loses _meta
Interface Definition
pkg/vmcp/types.go:247-263 - BackendClient interface returns simplified types
Handler Layer
pkg/vmcp/server/adapter/handler_factory.go:71-113 - CreateToolHandler
pkg/vmcp/server/adapter/handler_factory.go:115-169 - CreateResourceHandler
pkg/vmcp/server/adapter/handler_factory.go:171-244 - CreatePromptHandler
Request Parsing (Working Correctly)
pkg/mcp/parser.go:224-230 - Request _meta extraction ✅
pkg/mcp/parser.go:478-485 - GetMCPMeta context helper ✅
Proposed Solution
Option 1: Rich Response Types (Recommended)
Change BackendClient interface to return full MCP response structures:
type BackendClient interface {
// Returns full CallToolResult including _meta
CallTool(ctx context.Context, target *BackendTarget, toolName string, arguments map[string]any) (*mcp.CallToolResult, error)
// Returns full ReadResourceResult including _meta
ReadResource(ctx context.Context, target *BackendTarget, uri string) (*mcp.ReadResourceResult, error)
// Returns full GetPromptResult including _meta
GetPrompt(ctx context.Context, target *BackendTarget, name string, arguments map[string]any) (*mcp.GetPromptResult, error)
// ListCapabilities should also return full results with _meta
ListCapabilities(ctx context.Context, target *BackendTarget) (*CapabilityList, error)
}
Advantages:
- Full MCP spec compliance
- Preserves all response metadata
- Type-safe - compiler enforces correct usage
- Easier to add support for future MCP features
Disadvantages:
- Requires updating all
BackendClient implementations
- Breaking change to internal API
Option 2: Metadata Wrapper
Add metadata wrapper around results:
type ResponseWithMeta struct {
Result map[string]any
Meta map[string]any // Preserved _meta field
}
type BackendClient interface {
CallTool(...) (*ResponseWithMeta, error)
// ...
}
Advantages:
- Smaller change than Option 1
- Preserves existing result structure
Disadvantages:
- Not type-safe for different response types
- Still requires interface changes
- Less aligned with MCP specification types
Option 3: Context-Based Metadata
Store response metadata in context:
type responseMetaKey struct{}
func SetResponseMeta(ctx context.Context, meta map[string]any) context.Context {
return context.WithValue(ctx, responseMetaKey{}, meta)
}
func GetResponseMeta(ctx context.Context) map[string]any {
if meta, ok := ctx.Value(responseMetaKey{}).(map[string]any); ok {
return meta
}
return nil
}
Advantages:
- No interface changes required
- Can be added incrementally
Disadvantages:
- Implicit - easy to forget to propagate
- Context pollution
- Not idiomatic for response data
- Difficult to track metadata flow
Recommendation
Implement Option 1 (Rich Response Types) for these reasons:
- Spec Alignment: Directly aligns with MCP specification types
- Type Safety: Compiler enforces correct metadata handling
- Future-Proof: Easy to add new MCP features (e.g., structured tool outputs)
- Clarity: Explicit response structure, no hidden context magic
- Testability: Easy to verify metadata preservation in tests
Implementation Plan
Phase 1: Interface Update
- Update
BackendClient interface to return full MCP response types
- Update
httpBackendClient implementation to preserve _meta
- Update mock implementations for tests
Phase 2: Conversion Layer
- Remove simplified conversion logic in
pkg/vmcp/client/client.go
- Return full
mcp.CallToolResult, mcp.ReadResourceResult, etc.
- Preserve
_meta field from backend responses
Phase 3: Handler Adaptation
- Update handler factory to use full response types
- Forward
_meta from backend responses to client responses
- Merge/combine
_meta fields when aggregating multiple backends (if needed)
Phase 4: Testing
- Add tests for
_meta preservation in responses
- Add integration tests with real MCP servers
- Test distributed tracing flow
- Test progress token propagation
Phase 5: Documentation
- Update architecture documentation
- Document
_meta handling in vMCP
- Add examples for custom metadata
Testing Considerations
Unit Tests
func TestCallTool_PreservesMetaInResponse(t *testing.T) {
// Arrange: Backend returns response with _meta
expectedMeta := map[string]any{
"progressToken": "abc123",
"traceparent": "00-trace-id-01",
"custom.backend/hint": "cache-hit",
}
// Act: Call tool through vMCP
result, err := client.CallTool(ctx, target, "test_tool", args)
// Assert: _meta is preserved
require.NoError(t, err)
assert.Equal(t, expectedMeta, result._meta)
}
Integration Tests
func TestVMCP_EndToEndMetaPropagation(t *testing.T) {
// Test full flow: Client → vMCP → Backend → vMCP → Client
// Verify _meta propagates through entire chain
}
Compatibility Tests
func TestVMCP_BackwardCompatibility(t *testing.T) {
// Verify vMCP handles backends that don't return _meta
// (should not break, just omit _meta in response)
}
References
Additional Context
Current Request _meta Handling (For Comparison)
Request _meta is correctly handled:
// pkg/mcp/parser.go:224-230
var meta map[string]interface{}
if metaRaw, ok := paramsMap["_meta"]; ok {
if metaMap, ok := metaRaw.(map[string]interface{}); ok {
meta = metaMap // ✅ Preserved in context
}
}
We should apply the same preservation approach to response _meta.
MCP Specification Quote
From the spec:
Certain key names are reserved by MCP for protocol-level metadata... implementations MUST NOT make assumptions about values at these keys.
This means vMCP should act as a transparent proxy for _meta fields, preserving them without interpretation or modification (except for aggregation scenarios where merging may be needed).
Summary
The Virtual MCP Server (vMCP) is not preserving the
_metafield from backend MCP server responses when forwarding them to clients. According to the MCP specification,_metais a reserved field that can appear in both requests and responses for protocol-level metadata, but our current implementation only extracts_metafrom requests and discards it from responses.Problem Description
The Model Context Protocol specification defines
_metaas a universal metadata field that can appear in:JSONRPCRequest.params._meta)Resulttypes inherit_meta?: { [key: string]: unknown })JSONRPCNotification.params._meta)TextContent._meta,ImageContent._meta, etc.)Tool._meta,Resource._meta,Prompt._meta, etc.)Current Behavior:
_metafrom incoming request params (pkg/mcp/parser.go:224-230)_metaavailable via context (mcp.GetMCPMeta(ctx))_metafrom backend responses when converting them to simplified maps_metawhen constructing response objects for clientsExpected Behavior:
vMCP should preserve
_metafields from backend responses and forward them to clients, enabling:Root Cause Analysis
Architecture Flow
Code Analysis
1. Backend Response Structure (Correct)
Backends return full MCP response objects with
_meta:Reference: MCP Spec - CallToolResult schema
2. Client Conversion Layer (Bug Location)
File:
pkg/vmcp/client/client.go:391-476Problem: The conversion from
mcp.CallToolResulttomap[string]anyextracts only content fields and discards the_metafield entirely.3. Interface Definition (Design Issue)
File:
pkg/vmcp/types.go:247-263Problem: The interface returns simplified types (
map[string]any,[]byte,string) instead of full MCP response structures that would include_meta.4. Handler Factory (Cannot Fix Without Upstream Data)
File:
pkg/vmcp/server/adapter/handler_factory.go:71-113Problem: By the time we construct the response for the client,
_metahas already been discarded in the conversion layer.Impact Assessment
Affected Operations
All MCP operations that return structured results are affected:
Tool Calls (
tools/call)_metainCallToolResultResource Reads (
resources/read)_metainReadResourceResultPrompt Gets (
prompts/get)_metainGetPromptResultList Operations (
tools/list,resources/list,prompts/list)_metainListToolsResult,ListResourcesResult,ListPromptsResultCapability Discovery (
initialize)_metainInitializeResultMCP Specification Compliance
According to the MCP specification (2025-06-18):
All
Resulttypes in the spec inherit the baseResultinterface:vMCP is currently non-compliant with this aspect of the specification.
Use Cases Broken
Progress Reporting
progressTokenin responsesDistributed Tracing
traceparent) is lost in responsesCustom Backend Metadata
MCP Tasks (November 2025 Spec)
io.modelcontextprotocol/related-task) must be preservedAffected Code Locations
Core Issue
pkg/vmcp/client/client.go:391-476-CallToolconversion loses_metapkg/vmcp/client/client.go:479-534-ReadResourceconversion loses_metapkg/vmcp/client/client.go:535-596-GetPromptconversion loses_metaInterface Definition
pkg/vmcp/types.go:247-263-BackendClientinterface returns simplified typesHandler Layer
pkg/vmcp/server/adapter/handler_factory.go:71-113-CreateToolHandlerpkg/vmcp/server/adapter/handler_factory.go:115-169-CreateResourceHandlerpkg/vmcp/server/adapter/handler_factory.go:171-244-CreatePromptHandlerRequest Parsing (Working Correctly)
pkg/mcp/parser.go:224-230- Request_metaextraction ✅pkg/mcp/parser.go:478-485-GetMCPMetacontext helper ✅Proposed Solution
Option 1: Rich Response Types (Recommended)
Change
BackendClientinterface to return full MCP response structures:Advantages:
Disadvantages:
BackendClientimplementationsOption 2: Metadata Wrapper
Add metadata wrapper around results:
Advantages:
Disadvantages:
Option 3: Context-Based Metadata
Store response metadata in context:
Advantages:
Disadvantages:
Recommendation
Implement Option 1 (Rich Response Types) for these reasons:
Implementation Plan
Phase 1: Interface Update
BackendClientinterface to return full MCP response typeshttpBackendClientimplementation to preserve_metaPhase 2: Conversion Layer
pkg/vmcp/client/client.gomcp.CallToolResult,mcp.ReadResourceResult, etc._metafield from backend responsesPhase 3: Handler Adaptation
_metafrom backend responses to client responses_metafields when aggregating multiple backends (if needed)Phase 4: Testing
_metapreservation in responsesPhase 5: Documentation
_metahandling in vMCPTesting Considerations
Unit Tests
Integration Tests
Compatibility Tests
References
Additional Context
Current Request _meta Handling (For Comparison)
Request
_metais correctly handled:We should apply the same preservation approach to response
_meta.MCP Specification Quote
From the spec:
This means vMCP should act as a transparent proxy for
_metafields, preserving them without interpretation or modification (except for aggregation scenarios where merging may be needed).