MCP Server Design
PEN implements the Model Context Protocol with the MCP Go SDK v1.3.1.
Server Initialization
PEN creates the MCP server with an identity header and a few key options:
srv := mcp.NewServer(
&mcp.Implementation{Name: "pen", Version: version},
&mcp.ServerOptions{
Logger: logger,
Instructions: "PEN is an autonomous performance engineer for web applications. Use pen_ tools to profile, analyze, and debug frontend performance.",
KeepAlive: 30 * time.Second,
InitializedHandler: func(ctx context.Context, _ *mcp.InitializedRequest) {
logger.Info("MCP client connected and initialized")
},
},
)What matters here:
Instructionstells the LLM what PEN is and how to use it. Sent during theinitializehandshake.KeepAlivepings the transport periodically to catch dead sessions.InitializedHandlerfires once the client finishes the handshake.
Tool Registration
All 30 tools are registered at startup via tools.RegisterAll:
tools.RegisterAll(pen.Server(), &tools.Deps{
CDP: cdpClient,
Locks: pen.Locks(),
Limiter: security.NewRateLimiter(security.DefaultCooldowns),
Config: &tools.ToolsConfig{
AllowEval: *allowEval,
ProjectRoot: *projectRoot,
Version: version,
CDPPort: parseCDPPort(*cdpURL),
},
})The Deps struct bundles everything handlers need — no globals
anywhere. Tools are grouped by category (registerMemoryTools, registerCPUTools, etc.).
Typed Generic Handlers
The MCP Go SDK uses Go generics for type-safe tool handlers:
mcp.AddTool[InputType, any](server, toolDefinition, handlerFunc)Where InputType is a Go struct with jsonschema tags. The SDK builds the inputSchema from these tags automatically.
Handlers get the unmarshaled input directly — no manual JSON parsing.
Handler Signature
Every tool handler follows this pattern:
func makeToolHandler(deps *Deps) func(context.Context, *mcp.CallToolRequest, InputType) (*mcp.CallToolResult, any, error)The return values:
*mcp.CallToolResult— structured response withContent(text, images)any— unused (reserved for future SDK use)error— setsisError: trueon the MCP response
Transports
PEN supports three MCP transports:
| Transport | Flag | Use Case |
|---|---|---|
| stdio | --transport stdio (default) | IDE spawns PEN as child process |
| SSE | --transport sse | Browser-based or remote clients |
| HTTP | --transport http | Streamable HTTP; add --stateless to disable session tracking |
Both sse and http use mcp.NewStreamableHTTPHandler internally, mounted at /mcp. Default bind: localhost:6100.
Error Handling
Tool errors return through the SDK's error mechanism:
func toolError(msg string) (*mcp.CallToolResult, any, error) {
return nil, nil, errors.New(msg)
}The SDK sets isError: true on the response automatically. Errors
are written for LLM consumption — they explain what went wrong and what to try
next.
Example: "HeapProfiler is already in use by another operation. Wait for the current heap snapshot to finish, or call another tool in the meantime."
Concurrency
OperationLock
Domain-exclusive locking prevents conflicting CDP operations:
release, err := deps.Locks.Acquire("HeapProfiler")
if err != nil {
return toolError("HeapProfiler is already in use by another operation")
}
defer release()The lock never spans async boundaries — defer release() guarantees cleanup even on panics.
Rate Limiting
Heavy tools have cooldowns enforced before execution:
if err := deps.Limiter.Check("pen_heap_snapshot"); err != nil {
return toolError(err.Error())
}The limiter tracks the last execution time per tool. Record is called after successful execution.
Context Cancellation
Every handler respects ctx.Done(). If the client bails
mid-operation, CDP calls cancel, temp files clean up via defer,
and domain locks release.
Capabilities
PEN declares standard MCP server capabilities during the initialize handshake:
- Tools: Full
tools/listandtools/callsupport - Progress: Sends
notifications/progressfor slow operations (heap snapshots, traces) - No resources or prompts: PEN is tools-only — no MCP resources or prompt templates
pen init — Interactive Setup
PEN ships with an interactive setup wizard via pen init, built
with:
- charmbracelet/huh v1.0.0 — terminal form framework for the multi-step wizard
- charmbracelet/lipgloss v1.1.0 — terminal styling for formatted output
- charmbracelet/huh/spinner — loading animations during connection verification
The wizard sniffs out installed browsers and IDE configs, then writes the right MCP config file. It's the best way to get started.