项目文件夹

文件
Asim Aslam ab6f027741 Claude/update docs roadmap f zd2 j (#2866)
* Update docs and roadmap to March 2026 with focus priorities

- ROADMAP.md: Updated from Nov 2025 to reflect Q1 completions and current state
- ROADMAP_2026.md: Updated status to March 2026, added model package as delivered
- CURRENT_STATUS_SUMMARY.md: Rewrote with March 2026 status and clear next priorities
- PROJECT_STATUS_2026.md: Added model package section, updated recommendations
- Website roadmap: Updated Q3 security status and timestamps

Key focus areas identified: documentation guides, multi-protocol MCP,
LlamaIndex SDK, and OpenTelemetry integration.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* Add CLAUDE.md and four documentation guides to fill doc gaps

- CLAUDE.md: Project guide with structure, build commands, and priorities
- ai-native-services.md: End-to-end tutorial building an MCP-enabled task service
- mcp-security.md: Production security guide (auth, scopes, rate limiting, audit)
- tool-descriptions.md: Best practices for writing Go comments that help agents
- agent-patterns.md: Six integration patterns from single-agent to event-driven
- Updated docs index with new "AI & Agents" section linking all four guides

These were the highest priority gaps identified in the roadmap analysis:
the framework has solid features that were under-documented.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add OpenTelemetry tracing to MCP gateway

Integrate OpenTelemetry spans into the MCP gateway for both HTTP and
stdio transports. Each tool call now creates a server span with rich
attributes (tool name, account ID, auth outcome, transport type).
Trace context is propagated to downstream RPC calls via metadata,
enabling end-to-end distributed tracing through Jaeger, Grafana, etc.

New files:
- gateway/mcp/otel.go: Span creation, attribute constants, metadata carrier
- gateway/mcp/otel_test.go: 8 tests covering span creation, auth denied/allowed,
  rate limiting, trace propagation, noop provider, and missing token

Changes:
- Options.TraceProvider: Optional trace.TracerProvider field
- handleCallTool (HTTP): Creates OTel spans with auth/rate-limit attributes
- handleToolsCall (stdio): Same instrumentation for stdio transport
- go.mod: Added go.opentelemetry.io/otel/sdk v1.35.0 (test dependency)

The existing MCP trace ID (UUID) is preserved for backward compatibility
and recorded as a span attribute alongside the W3C trace context.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 08:16:06 +00:00

393 行
10 KiB
Go

package mcp
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/metadata"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
)
// StdioTransport implements MCP JSON-RPC 2.0 over stdio
// This is used by Claude Code and other local AI tools
type StdioTransport struct {
server *Server
reader *bufio.Reader
writer *bufio.Writer
writerMu sync.Mutex
ctx context.Context
cancel context.CancelFunc
}
// JSONRPCRequest represents a JSON-RPC 2.0 request
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// JSONRPCResponse represents a JSON-RPC 2.0 response
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
}
// RPCError represents a JSON-RPC error
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Standard JSON-RPC error codes
const (
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603
)
// NewStdioTransport creates a new stdio transport for the MCP server
func NewStdioTransport(server *Server) *StdioTransport {
ctx, cancel := context.WithCancel(context.Background())
return &StdioTransport{
server: server,
reader: bufio.NewReader(os.Stdin),
writer: bufio.NewWriter(os.Stdout),
ctx: ctx,
cancel: cancel,
}
}
// Serve starts the stdio transport and processes JSON-RPC requests
func (t *StdioTransport) Serve() error {
t.server.opts.Logger.Printf("[mcp] MCP server started (stdio transport)")
// Read and process requests from stdin
for {
select {
case <-t.ctx.Done():
return nil
default:
}
// Read one line (JSON-RPC request)
line, err := t.reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("failed to read request: %w", err)
}
// Parse JSON-RPC request
var req JSONRPCRequest
if err := json.Unmarshal(line, &req); err != nil {
t.sendError(nil, ParseError, "Parse error", err.Error())
continue
}
// Validate JSON-RPC version
if req.JSONRPC != "2.0" {
t.sendError(req.ID, InvalidRequest, "Invalid request", "jsonrpc must be '2.0'")
continue
}
// Handle request
go t.handleRequest(&req)
}
}
// handleRequest processes a single JSON-RPC request
func (t *StdioTransport) handleRequest(req *JSONRPCRequest) {
switch req.Method {
case "initialize":
t.handleInitialize(req)
case "tools/list":
t.handleToolsList(req)
case "tools/call":
t.handleToolsCall(req)
default:
t.sendError(req.ID, MethodNotFound, "Method not found", req.Method)
}
}
// handleInitialize handles the initialize request
func (t *StdioTransport) handleInitialize(req *JSONRPCRequest) {
result := map[string]interface{}{
"protocolVersion": "2024-11-05",
"capabilities": map[string]interface{}{
"tools": map[string]interface{}{},
},
"serverInfo": map[string]interface{}{
"name": "go-micro-mcp",
"version": "1.0.0",
},
}
t.sendResponse(req.ID, result)
}
// handleToolsList handles the tools/list request
func (t *StdioTransport) handleToolsList(req *JSONRPCRequest) {
t.server.toolsMu.RLock()
tools := make([]interface{}, 0, len(t.server.tools))
for _, tool := range t.server.tools {
tools = append(tools, map[string]interface{}{
"name": tool.Name,
"description": tool.Description,
"inputSchema": tool.InputSchema,
})
}
t.server.toolsMu.RUnlock()
result := map[string]interface{}{
"tools": tools,
}
t.sendResponse(req.ID, result)
}
// handleToolsCall handles the tools/call request
func (t *StdioTransport) handleToolsCall(req *JSONRPCRequest) {
// Parse params
var params struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
// Token allows callers to pass a bearer token for auth via the
// JSON-RPC params (since stdio has no HTTP headers).
Token string `json:"_token,omitempty"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
t.sendError(req.ID, InvalidParams, "Invalid params", err.Error())
return
}
// Get tool info
t.server.toolsMu.RLock()
tool, exists := t.server.tools[params.Name]
t.server.toolsMu.RUnlock()
if !exists {
t.sendError(req.ID, InvalidParams, "Tool not found", params.Name)
return
}
// Generate trace ID
traceID := uuid.New().String()
// Start OTel span (noop if TraceProvider is nil)
ctx, span := t.server.startToolSpan(t.ctx, params.Name, "stdio", traceID)
defer span.End()
// Authenticate and authorise (if Auth is configured)
var account *auth.Account
if t.server.opts.Auth != nil {
token := params.Token
if token == "" {
span.SetAttributes(attribute.Bool(AttrAuthAllowed, false), attribute.String(AttrAuthDeniedReason, "missing token"))
setSpanError(span, fmt.Errorf("missing token"))
t.server.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: params.Name, Allowed: false, DeniedReason: "missing token"})
t.sendError(req.ID, InvalidParams, "Unauthorized", "missing _token in params")
return
}
if strings.HasPrefix(token, "Bearer ") {
token = strings.TrimPrefix(token, "Bearer ")
}
acc, err := t.server.opts.Auth.Inspect(token)
if err != nil {
span.SetAttributes(attribute.Bool(AttrAuthAllowed, false), attribute.String(AttrAuthDeniedReason, "invalid token"))
setSpanError(span, fmt.Errorf("invalid token"))
t.server.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: params.Name, Allowed: false, DeniedReason: "invalid token"})
t.sendError(req.ID, InvalidParams, "Unauthorized", "invalid token")
return
}
account = acc
span.SetAttributes(attribute.String(AttrAccountID, account.ID))
// Check per-tool scopes
if len(tool.Scopes) > 0 {
span.SetAttributes(attribute.StringSlice(AttrScopesRequired, tool.Scopes))
if !hasScope(account.Scopes, tool.Scopes) {
span.SetAttributes(attribute.Bool(AttrAuthAllowed, false), attribute.String(AttrAuthDeniedReason, "insufficient scopes"))
setSpanError(span, fmt.Errorf("insufficient scopes"))
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: account.ID, ScopesRequired: tool.Scopes,
Allowed: false, DeniedReason: "insufficient scopes",
})
t.sendError(req.ID, InvalidParams, "Forbidden", "insufficient scopes")
return
}
}
}
// Rate limit check
if err := t.server.allowRate(params.Name); err != nil {
span.SetAttributes(attribute.Bool(AttrRateLimited, true))
setSpanError(span, err)
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, Allowed: false, DeniedReason: "rate limited",
})
t.sendError(req.ID, InternalError, "Rate limit exceeded", params.Name)
return
}
span.SetAttributes(attribute.Bool(AttrAuthAllowed, true))
// Convert arguments to JSON bytes for RPC call
inputBytes, err := json.Marshal(params.Arguments)
if err != nil {
t.sendError(req.ID, InternalError, "Failed to marshal arguments", err.Error())
return
}
// Build context with tracing metadata
// OTel trace context was already injected by startToolSpan; add MCP metadata.
md, _ := metadata.FromContext(ctx)
if md == nil {
md = make(metadata.Metadata)
}
md.Set(TraceIDKey, traceID)
md.Set(ToolNameKey, params.Name)
if account != nil {
md.Set(AccountIDKey, account.ID)
}
ctx = metadata.NewContext(ctx, md)
// Make RPC call
start := time.Now()
rpcReq := t.server.opts.Client.NewRequest(tool.Service, tool.Endpoint, &struct {
Data []byte
}{Data: inputBytes})
var rsp struct {
Data []byte
}
if err := t.server.opts.Client.Call(ctx, rpcReq, &rsp); err != nil {
setSpanError(span, err)
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start), Error: err.Error(),
})
t.sendError(req.ID, InternalError, "RPC call failed", err.Error())
return
}
setSpanOK(span)
// Audit successful call
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start),
})
// Parse response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err != nil {
// If unmarshal fails, return raw data
result = map[string]interface{}{
"data": string(rsp.Data),
}
}
t.sendResponse(req.ID, map[string]interface{}{
"content": []interface{}{
map[string]interface{}{
"type": "text",
"text": fmt.Sprintf("%v", result),
},
},
"trace_id": traceID,
})
}
// sendResponse sends a JSON-RPC response
func (t *StdioTransport) sendResponse(id interface{}, result interface{}) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
}
t.writeJSON(resp)
}
// sendError sends a JSON-RPC error response
func (t *StdioTransport) sendError(id interface{}, code int, message string, data interface{}) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Error: &RPCError{
Code: code,
Message: message,
Data: data,
},
}
t.writeJSON(resp)
}
// writeJSON writes a JSON-RPC message to stdout
func (t *StdioTransport) writeJSON(v interface{}) {
t.writerMu.Lock()
defer t.writerMu.Unlock()
data, err := json.Marshal(v)
if err != nil {
log.Printf("[mcp] Failed to marshal response: %v", err)
return
}
if _, err := t.writer.Write(data); err != nil {
log.Printf("[mcp] Failed to write response: %v", err)
return
}
if _, err := t.writer.Write([]byte("\n")); err != nil {
log.Printf("[mcp] Failed to write newline: %v", err)
return
}
if err := t.writer.Flush(); err != nil {
log.Printf("[mcp] Failed to flush writer: %v", err)
}
}
// Stop gracefully stops the stdio transport
func (t *StdioTransport) Stop() error {
t.cancel()
return nil
}