Summary
Follow-up from #421 / #422. MCP-backed tool spans now carry gen_ai.tool.type=extension and mcp.method.name=tools/call, but not the two remaining MCP span attributes from the semconv mcp.* namespace:
mcp.session.id — the server-assigned Mcp-Session-Id
mcp.protocol.version — the negotiated MCP wire-protocol version
They were deferred because the executor's ToolExecutor interface (forge-core/runtime/loop.go:41) only exposes Execute + ToolDefinitions() — it has no handle on the MCP connection state where a session id / protocol version live. This issue adds the plumbing.
Where the data lives
- Protocol version — pinned constant
mcp.ProtocolVersion = "2025-06-18" (forge-core/mcp/protocol.go:12); the client rejects a server whose protocolVersion differs (forge-core/mcp/client.go:177), so the pinned value is effectively the negotiated one per connection.
- Session id —
HTTPTransport.SessionID() returns the server-assigned Mcp-Session-Id (forge-core/mcp/transport_http.go:84), set during the handshake and replayed on each request (transport_http.go:124).
Neither is reachable from loop.go today: the tool is only known to the executor by name + the MCPSource marker (forge-core/tools/registry.go:39), and the concrete MCP tool object (which holds the client/transport) isn't surfaced through the registry.
Proposed design
-
Metadata interface on MCP tools. Add an optional interface the materialized MCP tool implements (it already holds the client/transport handle):
// in forge-core/tools
type MCPMetadata interface {
MCPSessionID() string // "" until the handshake assigns one
MCPProtocolVersion() string // negotiated/pinned wire version
}
(Either extend the existing MCPSource marker or add this alongside it.)
-
Registry accessor. Expose a lookup on tools.Registry and the runtime.ToolExecutor interface so the executor can query per-tool MCP metadata without importing mcp:
// ToolExecutor
MCPToolMeta(name string) (sessionID, protocolVersion string, ok bool)
The registry answers by type-asserting the registered tool to MCPMetadata; non-MCP tools return ok=false.
-
Stamp on the tool span. In loop.go, on the existing MCP branch (where mcp.method.name is already set), also set mcp.session.id / mcp.protocol.version when ok and non-empty. Add the two constants to forge-core/observability/attrs.go next to AttrMCPMethodName.
Session id may be empty for a server that doesn't use Mcp-Session-Id (stateless) — emit the attribute only when non-empty, consistent with the "absent key = not applicable" posture used elsewhere.
Acceptance criteria
References
No code changes in this issue — design + backlog.
Summary
Follow-up from #421 / #422. MCP-backed tool spans now carry
gen_ai.tool.type=extensionandmcp.method.name=tools/call, but not the two remaining MCP span attributes from the semconvmcp.*namespace:mcp.session.id— the server-assignedMcp-Session-Idmcp.protocol.version— the negotiated MCP wire-protocol versionThey were deferred because the executor's
ToolExecutorinterface (forge-core/runtime/loop.go:41) only exposesExecute+ToolDefinitions()— it has no handle on the MCP connection state where a session id / protocol version live. This issue adds the plumbing.Where the data lives
mcp.ProtocolVersion = "2025-06-18"(forge-core/mcp/protocol.go:12); the client rejects a server whoseprotocolVersiondiffers (forge-core/mcp/client.go:177), so the pinned value is effectively the negotiated one per connection.HTTPTransport.SessionID()returns the server-assignedMcp-Session-Id(forge-core/mcp/transport_http.go:84), set during the handshake and replayed on each request (transport_http.go:124).Neither is reachable from
loop.gotoday: the tool is only known to the executor by name + theMCPSourcemarker (forge-core/tools/registry.go:39), and the concrete MCP tool object (which holds the client/transport) isn't surfaced through the registry.Proposed design
Metadata interface on MCP tools. Add an optional interface the materialized MCP tool implements (it already holds the client/transport handle):
(Either extend the existing
MCPSourcemarker or add this alongside it.)Registry accessor. Expose a lookup on
tools.Registryand theruntime.ToolExecutorinterface so the executor can query per-tool MCP metadata without importingmcp:The registry answers by type-asserting the registered tool to
MCPMetadata; non-MCP tools returnok=false.Stamp on the tool span. In
loop.go, on the existing MCP branch (wheremcp.method.nameis already set), also setmcp.session.id/mcp.protocol.versionwhenokand non-empty. Add the two constants toforge-core/observability/attrs.gonext toAttrMCPMethodName.Session id may be empty for a server that doesn't use
Mcp-Session-Id(stateless) — emit the attribute only when non-empty, consistent with the "absent key = not applicable" posture used elsewhere.Acceptance criteria
tool.<server>__<tool>) carrymcp.protocol.version, andmcp.session.idwhen the server assigned one.function) tool spans carry neither.forge-core/runtime→forge-core/mcpimport cycle.attrs.go;observability-tracing.mdMCP row updated.mcp.session.idis omitted whilemcp.protocol.versionis present.References
forge-core/runtime/loop.go— tool span + existing MCP branch (mcp.method.name)forge-core/tools/registry.go:39—MCPSourcemarkerforge-core/mcp/transport_http.go:84—SessionID()forge-core/mcp/protocol.go:12— pinnedProtocolVersionforge-core/observability/attrs.go—AttrMCPMethodName(add session/protocol constants here)No code changes in this issue — design + backlog.