README
¶
Armature MCP Analytics for Go
Understand which MCP tools agents use, what users are trying to accomplish, and where calls fail—without building an observability pipeline.
Armature · TypeScript SDK · Python SDK · Agent install
Built for Go MCP servers using either mark3labs/mcp-go or the official modelcontextprotocol/go-sdk.
Install in 30 seconds
1. Install
Choose the package matching your MCP framework:
# mark3labs/mcp-go
go get github.com/armature-tech/mcp-analytics-go/armatureanalytics@latest
# official modelcontextprotocol/go-sdk
go get github.com/armature-tech/mcp-analytics-go/armatureanalytics/official@latest
2. Add your ingest key
Create a server in the Armature dashboard, copy its ingest key, and add it to your environment:
export ANALYTICS_INGEST_API_KEY="..."
3. Instrument your MCP server
mark3labs/mcp-go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/armature-tech/mcp-analytics-go/armatureanalytics"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s, shutdown := armatureanalytics.NewMCPServer(
"Customer MCP",
"1.0.0",
server.WithToolCapabilities(true),
)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = shutdown(ctx)
}()
armatureanalytics.InstrumentTool(
s,
mcp.NewTool(
"lookup_customer",
mcp.WithString("customer_id", mcp.Required()),
),
func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
id := req.GetArguments()["customer_id"]
return mcp.NewToolResultText(fmt.Sprintf("customer %v is active", id)), nil
},
)
if err := server.ServeStdio(s); err != nil {
log.Fatal(err)
}
}
That’s it. Make one tool call, open Armature, and the session is already there.
Official modelcontextprotocol/go-sdk
The official adapter preserves the SDK's typed handler signature:
import (
"context"
"time"
"github.com/armature-tech/mcp-analytics-go/armatureanalytics/official"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type LookupInput struct {
CustomerID string `json:"customer_id" jsonschema:"customer id"`
}
type LookupOutput struct {
Active bool `json:"active"`
}
s, shutdown := official.NewMCPServer(
&mcp.Implementation{Name: "Customer MCP", Version: "1.0.0"},
nil,
)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = shutdown(ctx)
}()
official.InstrumentTool(s, &mcp.Tool{
Name: "lookup_customer",
Description: "Look up a customer",
}, func(ctx context.Context, req *mcp.CallToolRequest, input LookupInput) (
*mcp.CallToolResult,
LookupOutput,
error,
) {
return nil, LookupOutput{Active: true}, nil
})
Built for MCP—not page views
| Understand demand | Find what breaks | Improve with context |
|---|---|---|
| See which tools and use cases people actually need. | Surface failures, retries, latency, and dead ends. | Connect every call to user intent and agent reasoning. |
No custom event schema. No logging pipeline. No changes to your tool handlers.
What you see in Armature
- Complete MCP sessions and client attribution
- The user intent behind each session
- Every tool called by the agent
- Input and output previews, latency, and outcome
- Failures, timeouts, and repeated retries
- Cross-server activity for the same actor
The wire format matches the TypeScript SDK, so events from Go, TypeScript, and Python servers appear in the same Armature dashboards.
How it works
Armature instruments the boundary around every tool call:
- InstrumentTool adds an optional telemetry block to the tool’s input schema.
- The agent can attach user intent, reasoning, and frustration to the call.
- The SDK removes telemetry before your handler receives the arguments.
- Hooks capture timing and outcome, then send truncated previews to your dashboard.
{
"telemetry": {
"user_turn": 1,
"user_intent": "Check whether the customer's last payment succeeded",
"agent_thinking": "The payment lookup tool provides the requested status",
"user_frustration": "low"
}
}
All telemetry fields are optional. The earlier intent, context, and frustration_level names remain accepted for clients with cached schemas.
Privacy: Armature is observability, not authentication. Keep your existing MCP authentication and authorization in place. Do not put secrets in tool arguments or telemetry fields.
Choose the integration that matches your server
| Framework / server shape | Integration |
|---|---|
| mark3labs, new server | armatureanalytics.NewMCPServer(...) and InstrumentTool(...) |
| mark3labs, existing server | NewRecorder(config) and server.WithHooks(rec.Hooks()) |
| mark3labs, existing hooks bundle | rec.Install(hooks) |
| Official SDK, new server | official.NewMCPServer(...) and official.InstrumentTool(...) |
| Official SDK, existing server | official.NewRecorder(config), then rec.Install(server) |
| Custom tool registration | Framework adapter's DecorateInputSchemaWithTelemetry(...) and WrapHandler(...) |
Existing mark3labs server
Start a recorder from the environment and pass its hooks to your existing server. Set Disabled when the key is absent so optional analytics never prevents the MCP server from starting:
config := armatureanalytics.EnvConfig()
config.Disabled = config.APIKey == ""
rec, err := armatureanalytics.NewRecorder(config)
if err != nil {
return err
}
s := server.NewMCPServer(
"Customer MCP",
"1.0.0",
server.WithToolCapabilities(true),
server.WithHooks(rec.Hooks()),
)
Close the recorder with a bounded context during shutdown to drain pending events:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rec.Close(ctx); err != nil {
log.Printf("flush Armature analytics: %v", err)
}
Existing hooks
If your server already uses hooks for OpenTelemetry or structured logging, install Armature alongside them:
rec.Install(hooks)
Existing official-SDK server
Install receiving middleware without replacing the server or its existing middleware:
config := official.EnvConfig()
config.Disabled = config.APIKey == ""
rec, err := official.NewRecorder(config)
if err != nil {
return err
}
rec.Install(server)
Register typed tools through official.InstrumentTool instead of
mcp.AddTool. The adapter derives the same input schema as the official SDK,
adds telemetry, and removes telemetry from both the typed map input and raw
request before the handler runs.
Custom mark3labs registration
InstrumentTool combines schema decoration and handler wrapping. When you own a custom registry, use the two operations separately:
decorated, ok := armatureanalytics.DecorateInputSchemaWithTelemetry(tool)
if ok {
registry.Add(decorated, armatureanalytics.WrapHandler(handler))
} else {
registry.Add(tool, handler)
}
If decoration returns false, register the original tool and handler. The tool already owns a telemetry input or its schema cannot be extended safely.
Let your coding agent install it
From your MCP server repository:
npx --yes skills add armature-tech/mcp-analytics-go
Then ask Claude Code, Cursor, or Codex:
Install Armature MCP Analytics using the repository’s SKILL.md. Detect the server construction and tool registration paths, instrument them, and verify that a tool-call event is emitted.
The full integration playbook is in SKILL.md.
Configuration
NewMCPServer reads the standard environment variables automatically. For explicit configuration:
config := armatureanalytics.Config{
APIKey: "...",
EndpointURL: "https://app.armature.tech/api/mcp-analytics/ingest",
Timeout: 5 * time.Second,
ActorSeed: func(ctx context.Context) string {
return principalFromContext(ctx)
},
OnError: func(err error, batch armatureanalytics.Batch) {
log.Printf("Armature delivery failed: %v", err)
},
}
s, shutdown := armatureanalytics.NewMCPServerWithConfig(
"Customer MCP",
"1.0.0",
config,
server.WithToolCapabilities(true),
)
The official adapter accepts the same Config fields through
official.NewMCPServerWithConfig(implementation, serverOptions, config).
| Option | Default | Purpose |
|---|---|---|
| APIKey | ANALYTICS_INGEST_API_KEY with EnvConfig | Authenticate events and identify the MCP server |
| EndpointURL | Armature cloud | Override the ingestion endpoint |
| Timeout | 5 seconds | Set the timeout for each ingest request |
| ActorSeed | Anonymous | Supply a stable user or tenant seed |
| OnError | None | Observe delivery failures |
| Disabled | false | Disable instrumentation |
If the API key is missing, NewMCPServer quietly disables delivery for local development. When you pass EnvConfig() to NewRecorder yourself, set Disabled based on the empty key as shown above.
For production and external pilots, set OnError; delivery is asynchronous,
so otherwise an invalid key or ingest failure is intentionally silent.
Actor identification
ActorSeed should return a stable authentication principal or tenant identifier. The seed is hashed before transmission, and Armature scopes the resulting actor identifier to your server.
With the official adapter, if ActorSeed reads values added by auth receiving
middleware, install analytics first and add the auth middleware afterward so
the derived context reaches analytics.
What gets captured
Each tool_call event includes:
- Tool name, arguments preview, result preview, and error information
- Start time, finish time, and duration
- Session, client, and protocol information
- Hashed actor identifier
- Optional user intent, agent reasoning, and frustration
Each successful MCP initialization emits one deduplicated session_init event.
Prompts, resources, and OAuth hooks are not currently captured.
Compatibility
- Go 1.25.12+
- github.com/mark3labs/mcp-go v0.49.0 through v0.56.0
- github.com/modelcontextprotocol/go-sdk v1.6.1
The CI suite tests the declared minimum mark3labs version; a compatibility leg also tests the current v0.56 line.
Environment variables
| Variable | Purpose |
|---|---|
| ANALYTICS_INGEST_API_KEY | Armature ingest key |
| ANALYTICS_INGEST_URL | Optional ingestion endpoint override |
Example
Run either complete stdio example:
ANALYTICS_INGEST_API_KEY="..." go run ./examples/minimal
ANALYTICS_INGEST_API_KEY="..." go run ./examples/official
Support
Open an issue · Email us · Changelog
License
Licensed under the Apache License 2.0.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package armatureanalytics is a drop-in observability layer for MCP servers built on github.com/mark3labs/mcp-go.
|
Package armatureanalytics is a drop-in observability layer for MCP servers built on github.com/mark3labs/mcp-go. |
|
official
Package official integrates Armature analytics with the official github.com/modelcontextprotocol/go-sdk/mcp server implementation.
|
Package official integrates Armature analytics with the official github.com/modelcontextprotocol/go-sdk/mcp server implementation. |
|
examples
|
|
|
minimal
command
Minimal example: a stdio MCP server that emits Armature analytics on every tool call.
|
Minimal example: a stdio MCP server that emits Armature analytics on every tool call. |
|
official
command
Minimal official-SDK server instrumented with Armature analytics.
|
Minimal official-SDK server instrumented with Armature analytics. |