dce725e69f
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
6.5 KiB
Markdown
152 lines
6.5 KiB
Markdown
---
|
|
name: mcp_server_http
|
|
kind: function
|
|
lang: go
|
|
domain: infra
|
|
version: "1.0.0"
|
|
purity: impure
|
|
signature: "func MCPHTTPHandler(opts MCPHTTPOpts) http.Handler"
|
|
description: "Devuelve un http.Handler que implementa el Streamable HTTP transport del protocolo MCP (spec 2025-03-26). Acepta POST con un mensaje JSON-RPC 2.0 unico y despacha initialize/tools/list/tools/call/ping al handler del usuario. Soporta auth opcional via MCPHTTPAuthFunc que enriquece el context antes de invocar el handler de tools."
|
|
tags: [mcp, http, rpc, json-rpc, tools, server, protocol, claude, backends]
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: "error_go_core"
|
|
imports:
|
|
- context
|
|
- encoding/json
|
|
- fmt
|
|
- io
|
|
- net/http
|
|
tested: true
|
|
tests:
|
|
- "Initialize retorna serverInfo con Name y Version correctos"
|
|
- "ToolsList retorna las tools registradas"
|
|
- "ToolsCall invoca handler y retorna content[0].text con el resultado"
|
|
- "BadAuth retorna 401 cuando opts.Auth devuelve error"
|
|
- "BodyTooLarge retorna 413 cuando el body supera 1 MiB"
|
|
- "ParseError retorna HTTP 200 con error JSON-RPC -32700 para body invalido"
|
|
- "Notification retorna 202 Accepted sin body cuando falta el campo id"
|
|
- "MethodNotAllowed retorna 405 para GET y DELETE"
|
|
test_file_path: "functions/infra/mcp_server_http_test.go"
|
|
file_path: "functions/infra/mcp_server_http.go"
|
|
params:
|
|
- name: opts.Name
|
|
desc: "Nombre del servidor reportado al cliente en la respuesta de initialize (serverInfo.name)."
|
|
- name: opts.Version
|
|
desc: "Version del servidor reportada al cliente en initialize (serverInfo.version)."
|
|
- name: opts.Tools
|
|
desc: "Lista de MCPToolDef (nombre, descripcion, JSON Schema del input) que el servidor expone. Mismo tipo que MCPServerOpts de mcp_server_stdio_go_infra."
|
|
- name: opts.Handler
|
|
desc: "Dispatcher unico para todas las tools. Recibe ctx (posiblemente enriquecido por Auth), nombre de la tool y arguments JSON crudo. Devuelve result, isError y err."
|
|
- name: opts.Auth
|
|
desc: "Funcion opcional de autenticacion. Recibe el *http.Request, devuelve un context enriquecido (p.ej. con user_id) o un error. Si devuelve error, el handler responde 401 sin invocar Handler. Si es nil no se hace auth."
|
|
- name: opts.Logger
|
|
desc: "Writer opcional para log de debug (p.ej. os.Stderr). Si es nil los mensajes se descartan."
|
|
output: "http.Handler listo para montarse en un mux. El handler es seguro para uso concurrente (no tiene estado mutable compartido)."
|
|
---
|
|
|
|
## Ejemplo
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"fn-registry/functions/infra"
|
|
)
|
|
|
|
func main() {
|
|
tools := []infra.MCPToolDef{
|
|
{
|
|
Name: "echo",
|
|
Description: "Devuelve el mensaje tal cual",
|
|
InputSchema: json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {"msg": {"type": "string"}},
|
|
"required": ["msg"]
|
|
}`),
|
|
},
|
|
}
|
|
|
|
toolHandler := func(ctx context.Context, name string, input json.RawMessage) (any, bool, error) {
|
|
if name == "echo" {
|
|
var args struct{ Msg string `json:"msg"` }
|
|
if err := json.Unmarshal(input, &args); err != nil {
|
|
return nil, false, err
|
|
}
|
|
return map[string]string{"result": args.Msg}, false, nil
|
|
}
|
|
return nil, true, fmt.Errorf("unknown tool: %s", name)
|
|
}
|
|
|
|
// AuthFunc Bearer simple: extrae el token, busca en DB, inyecta user_id en ctx.
|
|
// El kanban lo implementa asi, buscando en la tabla mcp_tokens.
|
|
authFn := func(r *http.Request) (context.Context, error) {
|
|
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
if token == "" {
|
|
return nil, fmt.Errorf("missing token")
|
|
}
|
|
// ... validar token en DB ...
|
|
ctx := context.WithValue(r.Context(), "user_id", "u_123")
|
|
return ctx, nil
|
|
}
|
|
|
|
mcpH := infra.MCPHTTPHandler(infra.MCPHTTPOpts{
|
|
Name: "my-app-mcp",
|
|
Version: "1.0.0",
|
|
Tools: tools,
|
|
Handler: toolHandler,
|
|
Auth: authFn,
|
|
Logger: os.Stderr,
|
|
})
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/mcp", mcpH)
|
|
|
|
fmt.Println("MCP HTTP server en :8300/mcp")
|
|
if err := http.ListenAndServe(":8300", mux); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
```
|
|
|
|
Para un cliente MCP HTTP, la configuracion en `.mcp.json` usa `type: http`:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"my-app": {
|
|
"type": "http",
|
|
"url": "http://localhost:8300/mcp",
|
|
"headers": {
|
|
"Authorization": "Bearer <token>"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Cuando usarla
|
|
|
|
Cuando necesites exponer tools MCP a Claude o a un agente via HTTP en lugar de via stdio — es decir, cuando el servidor MCP es un proceso separado (no un subproceso del cliente) o cuando varios clientes deben compartir el mismo servidor. Tipico en apps con backend Go ya existente (kanban, sqlite_api, registry_api) que quieren aceptar llamadas MCP sin lanzar un subproceso nuevo por sesion.
|
|
|
|
Usa `mcp_server_stdio_go_infra` si el cliente lanza el servidor como subproceso (Claude Desktop, `claude -p`). Usa esta funcion si el servidor ya esta corriendo y el cliente se conecta via URL.
|
|
|
|
## Gotchas
|
|
|
|
- **Sin sesiones MCP (Mcp-Session-Id)**: la spec 2025-03-26 define un header `Mcp-Session-Id` para multiplexar sesiones sobre el mismo endpoint. Esta implementacion no lo emite ni lo exige — cada POST es independiente. TODO: implementar sesiones cuando se necesiten mas de un cliente concurrente con estado de sesion separado.
|
|
- **GET (SSE server→client) no implementado**: POST cubre el 100% de los casos de uso actuales (tools call). El canal SSE (server-initiated notifications) se puede anadir montando un segundo handler GET en el mismo path. Devuelve 405 hasta entonces.
|
|
- **DELETE (close session) no implementado**: 405. Sin sesiones, no hay nada que cerrar.
|
|
- **Body limit 1 MiB**: requests mas grandes reciben 413. Si tus tools reciben inputs grandes (imagenes en base64, JSONs voluminosos), ajusta `mcpHTTPBodyLimit` en el .go o recibe los datos por referencia (URL/path) en vez de inline.
|
|
- **CORS no incluido**: monta `http_cors_middleware_go_infra` en el mux antes del handler si el cliente MCP es un frontend web o viene de origen diferente.
|
|
- **Errores de protocolo usan HTTP 200**: segun el spec MCP, los errores JSON-RPC se devuelven con HTTP 200 para que el cliente pueda parsear el objeto error. Solo `401` y `413` son codigos HTTP de error.
|