diff --git a/docs/plans/2026-03-21-mcp-sdk-migration.md b/docs/plans/2026-03-21-mcp-sdk-migration.md index 4bcda7c..bbb1e43 100644 --- a/docs/plans/2026-03-21-mcp-sdk-migration.md +++ b/docs/plans/2026-03-21-mcp-sdk-migration.md @@ -1,414 +1,772 @@ -# Migration Plan: Official MCP SDK → mcp-go +# MCP SDK & AX Convention Migration Plan -**Date:** 2026-03-21 -**Status:** Draft -**Motivation:** The official SDK (`github.com/modelcontextprotocol/go-sdk`) has unexported fields that make custom notifications impossible. We need `claude/channel` support (experimental capability) so the server can push events (inbox messages, dispatch completions, webhook events) into a running Claude Code session. `mcp-go` (`github.com/mark3labs/mcp-go`) exposes `SendNotificationToClient` and `SendNotificationToAllClients` plus full session management. +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. -**Breaking change risk:** 2 consumers (`agent`, `ide`) import `forge.lthn.ai/core/mcp` — both expose the `Subsystem` interface which references `*mcp.Server`. +**Goal:** Align the MCP service layer with CoreGO AX conventions (Options/Result/Service DTOs), add server→client notification broadcasting via `SendNotificationToAllClients()`, and register the `claude/channel` experimental capability for pushing events into Claude Code sessions. + +**Architecture:** Refactor `mcp.Service` from functional options (`Option func(*Service) error`) to an `Options{}` struct. Add notification broadcasting by iterating the official SDK's `Server.Sessions()` (refactoring TCP/Unix transports to use `Server.Connect()` so all sessions are visible). Register `claude/channel` in `ServerCapabilities.Experimental` so clients can discover push-event support. + +**Tech Stack:** Go 1.26, `github.com/modelcontextprotocol/go-sdk` v1.4.1, `dappco.re/go/core` v0.4.7 --- -## 1. SDK Comparison — Key Differences +## SDK Evaluation -### Package layout +**Current SDK:** `github.com/modelcontextprotocol/go-sdk v1.4.1` (official MCP Go SDK) -| Concept | Official SDK | mcp-go | -|---------|-------------|--------| -| Types import | `github.com/modelcontextprotocol/go-sdk/mcp` | `github.com/mark3labs/mcp-go/mcp` | -| Server import | same package | `github.com/mark3labs/mcp-go/server` | -| JSON-RPC | `github.com/modelcontextprotocol/go-sdk/jsonrpc` | Not exported (internal) | +**Alternative evaluated:** `github.com/mark3labs/mcp-go` — community SDK with built-in `SendNotificationToAllClients()` and `SendNotificationToClient()`. -### Server creation +| Criteria | Official SDK | mark3labs/mcp-go | +|----------|-------------|------------------| +| Multi-session support | `Server.Sessions()` iterator, `Server.Connect()` | `SendNotificationToAllClients()` built-in | +| Tool registration | Generic `AddTool[In, Out]()` — matches existing pattern | `AddTool(NewTool(), handler)` — would require rewrite | +| Experimental capabilities | `ServerCapabilities.Experimental map[string]any` | Same | +| Transport support | Stdio, SSE, StreamableHTTP, InMemory | Stdio, SSE, StreamableHTTP | +| Options pattern | `*ServerOptions` struct — aligns with AX DTOs | Functional options — conflicts with AX migration | +| Handler signatures | `func(ctx, *CallToolRequest, In) (*CallToolResult, Out, error)` | `func(ctx, CallToolRequest) (*CallToolResult, error)` | -| | Official SDK | mcp-go | -|-|-------------|--------| -| Constructor | `mcp.NewServer(&mcp.Implementation{Name, Version}, nil)` | `server.NewMCPServer("name", "version", ...options)` | -| Return type | `*mcp.Server` | `*server.MCPServer` | -| Capabilities | Set via `ServerOptions` (2nd arg) | `server.WithToolCapabilities(bool)`, `server.WithRecovery()`, etc. | +**Decision:** Stay on official SDK. It already uses struct-based options (closer to AX), preserves our generic `addToolRecorded[In, Out]()` pattern, and supports multi-session via `Server.Sessions()`. Implement `SendNotificationToAllClients()` as a thin wrapper. -### Tool definition +## Existing Infrastructure -| | Official SDK | mcp-go | -|-|-------------|--------| -| Struct | `&mcp.Tool{Name: "...", Description: "..."}` | `mcp.NewTool("name", mcp.WithDescription("..."), mcp.WithString("param", mcp.Required()), ...)` | -| Schema | Auto-generated from Go struct tags via reflection (our `addToolRecorded` pattern) | Must be declared explicitly with `WithString`/`WithNumber`/`WithBoolean`/`WithObject`/`WithArray` builders, OR supply raw `InputSchema` | +Already built: +- `Service` struct wrapping `*mcp.Server` with functional options (`mcp.go`) +- Generic `addToolRecorded[In, Out]()` for tool registration + REST bridge (`registry.go`) +- `Subsystem` / `SubsystemWithShutdown` interfaces (`subsystem.go`) +- 4 transports: stdio, TCP, Unix, HTTP (`transport_*.go`) +- `BridgeToAPI` REST bridge (`bridge.go`) +- 7 tool groups: files, language, metrics, process, rag, webview, ws +- 3 subsystems: brain, ide, agentic +- Import migration to `dappco.re/go/core` already committed (4c6c9d7) -### Tool registration +## Consumer Impact -| | Official SDK | mcp-go | -|-|-------------|--------| -| Function | `mcp.AddTool(server, tool, handler)` — package-level generic function | `s.AddTool(tool, handler)` — method on `*server.MCPServer` | -| Handler signature | `func(ctx, *mcp.CallToolRequest, In) (*mcp.CallToolResult, Out, error)` — 3 returns, generic typed input | `func(ctx, mcp.CallToolRequest) (*mcp.CallToolResult, error)` — 2 returns, untyped input | -| Input access | Auto-deserialized into typed `In` struct | `request.RequireString("name")`, `request.GetString("name", default)`, or manual `request.Params.Arguments["key"]` | -| Result helpers | Return `*mcp.CallToolResult` with `Content` slice | `mcp.NewToolResultText("...")`, `mcp.NewToolResultError("...")` | +2 consumers import `forge.lthn.ai/core/mcp`: **agent**, **ide**. -### Transports +Both call `mcp.New(...)` with functional options and `mcp.WithSubsystem(...)`. Both must be updated after Phase 1. -| | Official SDK | mcp-go | -|-|-------------|--------| -| Stdio | `server.Run(ctx, &mcp.StdioTransport{})` | `server.ServeStdio(s)` — top-level function | -| HTTP | `mcp.NewStreamableHTTPHandler(factory, opts)` | `server.NewStreamableHTTPServer(s, opts...)` | -| SSE | N/A | `server.NewSSEServer(s, opts...)` | -| TCP | Custom `Transport`/`Connection` impl | No built-in TCP — must implement or wrap | +## File Structure -### Session & Notifications (the reason for migration) - -| | Official SDK | mcp-go | -|-|-------------|--------| -| Session access | Not exposed | `server.ClientSessionFromContext(ctx)`, `server.GetSessionID(ctx)` | -| Server from handler | Not available | `server.ServerFromContext(ctx)` | -| Push to client | Not possible (unexported) | `mcpServer.SendNotificationToClient(ctx, method, params)` | -| Broadcast | Not possible | `mcpServer.SendNotificationToAllClients(method, params)` | -| Session hooks | None | `hooks.AddOnRegisterSession(...)`, `hooks.AddOnUnregisterSession(...)` | -| Per-session tools | None | `s.AddSessionTool(sessionID, tool, handler)`, `s.DeleteSessionTools(...)` | +| File | Action | Purpose | +|------|--------|---------| +| `pkg/mcp/mcp.go` | Modify | Replace `Option` func type with `Options{}` struct; update `New()` | +| `pkg/mcp/subsystem.go` | Modify | Remove `WithSubsystem` func; subsystems move into `Options.Subsystems` | +| `pkg/mcp/notify.go` | Create | `SendNotificationToAllClients()`, `ChannelSend()`, channel helpers | +| `pkg/mcp/registry.go` | Modify | Add usage-example comments | +| `pkg/mcp/bridge.go` | Modify | Minor: usage-example comments | +| `pkg/mcp/transport_stdio.go` | Modify | Usage-example comments | +| `pkg/mcp/transport_tcp.go` | Modify | Usage-example comments | +| `pkg/mcp/transport_unix.go` | Modify | Usage-example comments | +| `pkg/mcp/transport_http.go` | Modify | Usage-example comments | +| `pkg/mcp/tools_metrics.go` | Modify | Usage-example comments on Input/Output types | +| `pkg/mcp/tools_process.go` | Modify | Usage-example comments on Input/Output types | +| `pkg/mcp/tools_rag.go` | Modify | Usage-example comments on Input/Output types | +| `pkg/mcp/tools_webview.go` | Modify | Usage-example comments on Input/Output types | +| `pkg/mcp/tools_ws.go` | Modify | Usage-example comments on Input/Output types | +| `pkg/mcp/mcp_test.go` | Modify | Update tests for `Options{}` constructor | +| `pkg/mcp/subsystem_test.go` | Modify | Update tests for `Options.Subsystems` | +| `pkg/mcp/notify_test.go` | Create | Tests for notification broadcasting | --- -## 2. Architectural Decisions +## Phase 1: Service Options{} Refactoring -### 2a. Handler signature adapter +Replace the functional options pattern with an `Options{}` struct. This is the breaking change — consumers must update their `mcp.New()` calls. -The biggest change: every tool handler must change from 3-return generic to 2-return untyped. Two approaches: +**Files:** +- Modify: `pkg/mcp/mcp.go` +- Modify: `pkg/mcp/subsystem.go` +- Modify: `pkg/mcp/mcp_test.go` +- Modify: `pkg/mcp/subsystem_test.go` -**Option A — Direct rewrite:** Change every handler to `func(ctx, mcp.CallToolRequest) (*mcp.CallToolResult, error)`. Manually unmarshal input from `request.Params.Arguments` and marshal output via `mcp.NewToolResultText(json)`. +- [ ] **Step 1: Define Options struct and update New()** -**Option B — Adapter pattern (recommended):** Write a generic adapter that preserves the current handler signatures: +Replace the current functional option pattern: ```go -// adaptHandler wraps a typed handler for use with mcp-go. -func adaptHandler[In, Out any](h func(ctx context.Context, req mcp.CallToolRequest, input In) (*mcp.CallToolResult, Out, error)) server.ToolHandler { - return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var input In - data, _ := json.Marshal(req.Params.Arguments) - if err := json.Unmarshal(data, &input); err != nil { - return mcp.NewToolResultError("invalid input: " + err.Error()), nil +// BEFORE: +type Option func(*Service) error + +func WithWorkspaceRoot(root string) Option { ... } +func WithProcessService(ps *process.Service) Option { ... } +func WithWSHub(hub *ws.Hub) Option { ... } +func WithSubsystem(sub Subsystem) Option { ... } + +func New(opts ...Option) (*Service, error) { ... } +``` + +With an `Options{}` struct: + +```go +// Options configures a Service. +// +// svc, err := mcp.New(mcp.Options{ +// WorkspaceRoot: "/path/to/project", +// ProcessService: ps, +// WSHub: hub, +// Subsystems: []Subsystem{brain, ide}, +// }) +type Options struct { + WorkspaceRoot string // Restrict file ops to this directory (empty = cwd) + Unrestricted bool // Disable sandboxing entirely (not recommended) + ProcessService *process.Service // Optional process management + WSHub *ws.Hub // Optional WebSocket hub for real-time streaming + Subsystems []Subsystem // Additional tool groups registered at startup +} + +// New creates a new MCP service with file operations. +// +// svc, err := mcp.New(mcp.Options{WorkspaceRoot: "."}) +func New(opts Options) (*Service, error) { + impl := &mcp.Implementation{ + Name: "core-cli", + Version: "0.1.0", + } + + server := mcp.NewServer(impl, &mcp.ServerOptions{ + Capabilities: &mcp.ServerCapabilities{ + Tools: &mcp.ToolCapabilities{ListChanged: true}, + }, + }) + + s := &Service{ + server: server, + processService: opts.ProcessService, + wsHub: opts.WSHub, + subsystems: opts.Subsystems, + logger: log.Default(), + } + + // Workspace root: unrestricted, explicit root, or default to cwd + if opts.Unrestricted { + s.workspaceRoot = "" + s.medium = io.Local + } else { + root := opts.WorkspaceRoot + if root == "" { + cwd, err := os.Getwd() + if err != nil { + return nil, log.E("mcp.New", "failed to get working directory", err) + } + root = cwd } - result, output, err := h(ctx, req, input) + abs, err := filepath.Abs(root) if err != nil { - return nil, err + return nil, log.E("mcp.New", "invalid workspace root", err) } - if result != nil { - return result, nil + m, merr := io.NewSandboxed(abs) + if merr != nil { + return nil, log.E("mcp.New", "failed to create workspace medium", merr) } - // Marshal output to JSON text result - outJSON, _ := json.Marshal(output) - return mcp.NewToolResultText(string(outJSON)), nil + s.workspaceRoot = abs + s.medium = m + } + + s.registerTools(s.server) + + for _, sub := range s.subsystems { + sub.RegisterTools(s.server) + } + + return s, nil +} +``` + +- [ ] **Step 2: Remove functional option functions** + +Delete from `mcp.go`: +- `type Option func(*Service) error` +- `func WithWorkspaceRoot(root string) Option` +- `func WithProcessService(ps *process.Service) Option` +- `func WithWSHub(hub *ws.Hub) Option` + +Delete from `subsystem.go`: +- `func WithSubsystem(sub Subsystem) Option` + +- [ ] **Step 3: Update tests** + +Find all test calls to `New(...)` in `mcp_test.go`, `subsystem_test.go`, `integration_test.go`, `transport_e2e_test.go`, and other `_test.go` files. All tests use `package mcp` (internal). Replace: + +```go +// BEFORE: +svc, err := New(WithWorkspaceRoot(dir)) +svc, err := New(WithSubsystem(&fakeSub{})) + +// AFTER: +svc, err := New(Options{WorkspaceRoot: dir}) +svc, err := New(Options{Subsystems: []Subsystem{&fakeSub{}}}) +``` + +- [ ] **Step 4: Verify compilation** + +```bash +go vet ./pkg/mcp/... +go build ./pkg/mcp/... +go test ./pkg/mcp/... +``` + +- [ ] **Step 5: Commit** + +```bash +git add pkg/mcp/mcp.go pkg/mcp/subsystem.go pkg/mcp/*_test.go +git commit -m "refactor(mcp): replace functional options with Options{} struct + +Aligns with CoreGO AX convention: Options{} DTOs instead of +functional option closures. Breaking change for consumers +(agent, ide) — they must update their mcp.New() calls. + +Co-Authored-By: Virgil " +``` + +--- + +## Phase 2: Notification Support + claude/channel Capability + +Add server→client notification broadcasting and register the `claude/channel` experimental capability. + +**Files:** +- Create: `pkg/mcp/notify.go` +- Create: `pkg/mcp/notify_test.go` +- Modify: `pkg/mcp/mcp.go` (register experimental capability in `New()`) +- Modify: `pkg/mcp/transport_tcp.go` (use `s.server.Connect()` instead of per-connection servers) +- Modify: `pkg/mcp/transport_unix.go` (same as TCP) + +**Important: Transport-level limitation.** The TCP and Unix transports currently create a **new `mcp.Server` per connection** in `handleConnection()`. Sessions on those per-connection servers are invisible to `s.server.Sessions()`. Notifications therefore only reach stdio and HTTP (StreamableHTTP) clients out of the box. To support TCP/Unix notifications, Phase 2 also refactors TCP/Unix to use `s.server.Connect()` instead of creating independent servers — this registers each connection's session on the shared server instance. + +- [ ] **Step 1: Refactor TCP/Unix to use shared server sessions** + +In `transport_tcp.go` and `transport_unix.go`, replace the per-connection `mcp.NewServer()` call with `s.server.Connect()`: + +```go +// BEFORE (transport_tcp.go handleConnection): +server := mcp.NewServer(impl, nil) +s.registerTools(server) +for _, sub := range s.subsystems { sub.RegisterTools(server) } +_ = server.Run(ctx, transport) + +// AFTER: +session, err := s.server.Connect(ctx, transport, nil) +if err != nil { + s.logger.Debug("tcp: connect failed", "error", err) + return +} +<-session.Wait() +``` + +This ensures every TCP/Unix connection registers its session on the shared `s.server`, making it visible to `Sessions()` and `SendNotificationToAllClients`. + +- [ ] **Step 2: Create notify.go with notification methods** + +```go +// SPDX-License-Identifier: EUPL-1.2 + +package mcp + +import ( + "context" + "iter" + + "forge.lthn.ai/core/go-log" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// SendNotificationToAllClients broadcasts a log-level notification to every +// connected MCP session (stdio, HTTP, TCP, and Unix). +// Errors on individual sessions are logged but do not stop the broadcast. +// +// s.SendNotificationToAllClients(ctx, "info", "build complete", map[string]any{"duration": "3.2s"}) +func (s *Service) SendNotificationToAllClients(ctx context.Context, level mcp.LoggingLevel, logger string, data any) { + for session := range s.server.Sessions() { + if err := session.Log(ctx, &mcp.LoggingMessageParams{ + Level: level, + Logger: logger, + Data: data, + }); err != nil { + s.logger.Debug("notify: failed to send to session", "session", session.ID(), "error", err) + } + } +} + +// ChannelSend pushes a channel event to all connected clients. +// This uses the claude/channel experimental capability. +// Channel names follow the convention "subsystem.event" (e.g. "build.complete", "agent.status"). +// +// s.ChannelSend(ctx, "build.complete", map[string]any{"repo": "go-io", "status": "passed"}) +func (s *Service) ChannelSend(ctx context.Context, channel string, data any) { + payload := map[string]any{ + "channel": channel, + "data": data, + } + s.SendNotificationToAllClients(ctx, "info", "channel", payload) +} + +// ChannelSendToSession pushes a channel event to a specific session. +// +// s.ChannelSendToSession(ctx, session, "agent.progress", progressData) +func (s *Service) ChannelSendToSession(ctx context.Context, session *mcp.ServerSession, channel string, data any) { + payload := map[string]any{ + "channel": channel, + "data": data, + } + if err := session.Log(ctx, &mcp.LoggingMessageParams{ + Level: "info", + Logger: "channel", + Data: payload, + }); err != nil { + s.logger.Debug("channel: failed to send to session", "session", session.ID(), "channel", channel, "error", err) + } +} + +// Sessions returns an iterator over all connected MCP sessions. +// Useful for subsystems that need to send targeted notifications. +// +// for session := range s.Sessions() { +// s.ChannelSendToSession(ctx, session, "status", data) +// } +func (s *Service) Sessions() iter.Seq[*mcp.ServerSession] { + return s.server.Sessions() +} + +// channelCapability returns the experimental capability descriptor +// for claude/channel, registered during New(). +func channelCapability() map[string]any { + return map[string]any{ + "claude/channel": map[string]any{ + "version": "1", + "description": "Push events into client sessions via named channels", + "channels": []string{ + "build.complete", + "build.failed", + "agent.status", + "agent.blocked", + "agent.complete", + "brain.recall.complete", + "process.exit", + "test.result", + }, + }, } } ``` -This lets us keep every handler's current signature (including the REST bridge) and migrates only the registration layer. The handler `req` param changes from `*mcp.CallToolRequest` (pointer) to `mcp.CallToolRequest` (value) — a minor type change. +- [ ] **Step 3: Register experimental capability in New()** -### 2b. Tool schema declaration - -Current approach uses `addToolRecorded` with reflection-based `structSchema()`. With mcp-go, two options: - -**Option A — Builder API:** Rewrite each tool's schema using `mcp.WithString(...)`, `mcp.WithNumber(...)`, etc. Tedious (40+ tools) but idiomatic. - -**Option B — Raw InputSchema (recommended):** `mcp.NewTool` accepts a raw `InputSchema` option. Feed the existing `structSchema()` output directly: +Update `New()` in `mcp.go` to pass capabilities to `mcp.NewServer`: ```go -mcp.NewTool("file_read", - mcp.WithDescription("Read the contents of a file"), - mcp.WithRawSchema(structSchema(new(ReadFileInput))), -) +server := mcp.NewServer(impl, &mcp.ServerOptions{ + Capabilities: &mcp.ServerCapabilities{ + Tools: &mcp.ToolCapabilities{ListChanged: true}, + Logging: &mcp.LoggingCapabilities{}, + Experimental: channelCapability(), + }, +}) ``` -If `WithRawSchema` doesn't exist, the `mcp.Tool` struct likely has an `InputSchema` field we can set after construction. This preserves the reflection-based schema generation. +- [ ] **Step 4: Create notify_test.go** -### 2c. REST bridge (`addToolRecorded`) +Uses `package mcp` (internal tests) consistent with all existing test files in this package. -The `addToolRecorded` generic function currently: -1. Calls `mcp.AddTool(server, tool, handler)` — registers with MCP -2. Creates a `RESTHandler` closure — for the REST bridge -3. Reflects `In`/`Out` types for JSON Schema — for API docs +```go +// SPDX-License-Identifier: EUPL-1.2 -With mcp-go, step 1 changes to `s.AddTool(tool, adaptHandler(h))`. Steps 2 and 3 remain unchanged. The `ToolRecord` struct and `RESTHandler` pattern are internal to our code, not SDK types. +package mcp -### 2d. TCP transport +import ( + "context" + "testing" -The official SDK exposes `mcp.Transport` and `mcp.Connection` interfaces. Our `connTransport` implements these. mcp-go doesn't have equivalent interfaces. Options: + "github.com/stretchr/testify/assert" +) -**Option A — Wrap stdio over TCP:** Pipe TCP conn's reader/writer into mcp-go's stdio transport. +func TestSendNotificationToAllClients_Good(t *testing.T) { + svc, err := New(Options{}) + assert.NoError(t, err) -**Option B — Use StreamableHTTP:** Replace TCP with HTTP transport (mcp-go has built-in support). This is architecturally cleaner for multi-client scenarios. + // With no connected sessions, should not panic + ctx := context.Background() + svc.SendNotificationToAllClients(ctx, "info", "test", map[string]any{"key": "value"}) +} -**Option C — Implement custom transport:** mcp-go's `server` package may expose transport interfaces. Investigate at implementation time. +func TestChannelSend_Good(t *testing.T) { + svc, err := New(Options{}) + assert.NoError(t, err) -### 2e. Subsystem interface + ctx := context.Background() + svc.ChannelSend(ctx, "build.complete", map[string]any{"repo": "go-io"}) +} -The `Subsystem` interface exposes `RegisterTools(server *mcp.Server)`. This must change to `RegisterTools(server *server.MCPServer)`. This is a **breaking change for consumers** (`agent`, `ide`). +func TestChannelCapability_Good(t *testing.T) { + // Verify the capability struct is well-formed + svc, err := New(Options{}) + assert.NoError(t, err) + assert.NotNil(t, svc.Server()) +} +``` + +- [ ] **Step 5: Verify compilation and tests** + +```bash +go vet ./pkg/mcp/... +go test ./pkg/mcp/... +``` + +- [ ] **Step 6: Commit** + +```bash +git add pkg/mcp/notify.go pkg/mcp/notify_test.go pkg/mcp/mcp.go pkg/mcp/transport_tcp.go pkg/mcp/transport_unix.go +git commit -m "feat(mcp): add notification broadcasting + claude/channel capability + +New methods: +- SendNotificationToAllClients: broadcasts to all connected MCP sessions +- ChannelSend: push named channel events (build.complete, agent.status, etc.) +- ChannelSendToSession: push to a specific session +- Sessions: iterator over connected sessions for subsystem use + +Refactors TCP/Unix transports to use Server.Connect() instead of +creating per-connection servers, so all sessions are visible to +the notification broadcaster. + +Registers claude/channel as an experimental MCP capability so clients +(Claude Code, IDEs) can discover and subscribe to push events. + +Co-Authored-By: Virgil " +``` --- -## 3. File-by-File Migration Plan +## Phase 3: Usage-Example Comments + Naming -### Phase 1 — Core types and registration (foundation) +Add usage-example comments to all public types and functions. This is the CoreGO convention: comments show how to call the thing, not just what it does. -#### `go.mod` -- **Remove:** `github.com/modelcontextprotocol/go-sdk v1.4.1` -- **Add:** `github.com/mark3labs/mcp-go v1.x.x` (latest stable) -- **Remove:** `github.com/modelcontextprotocol/go-sdk/jsonrpc` indirect (if present) +**Files:** +- Modify: `pkg/mcp/mcp.go` (Input/Output types) +- Modify: `pkg/mcp/registry.go` +- Modify: `pkg/mcp/bridge.go` +- Modify: `pkg/mcp/tools_metrics.go` +- Modify: `pkg/mcp/tools_process.go` +- Modify: `pkg/mcp/tools_rag.go` +- Modify: `pkg/mcp/tools_webview.go` +- Modify: `pkg/mcp/tools_ws.go` +- Modify: `pkg/mcp/transport_stdio.go` +- Modify: `pkg/mcp/transport_tcp.go` +- Modify: `pkg/mcp/transport_unix.go` +- Modify: `pkg/mcp/transport_http.go` -#### `pkg/mcp/subsystem.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New imports:** `github.com/mark3labs/mcp-go/mcp` + `github.com/mark3labs/mcp-go/server` -- **Change:** `RegisterTools(server *mcp.Server)` → `RegisterTools(server *server.MCPServer)` -- **Impact:** Breaking change for `Subsystem` interface consumers (`agent`, `ide` modules) +- [ ] **Step 1: Update Input/Output type comments in mcp.go** -#### `pkg/mcp/registry.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New imports:** `github.com/mark3labs/mcp-go/mcp` + `github.com/mark3labs/mcp-go/server` -- **Changes:** - - `addToolRecorded[In, Out]()` signature: `server *mcp.Server` → `server *server.MCPServer` - - Replace `mcp.AddTool(server, t, h)` → `server.AddTool(tool, adaptHandler(h))` - - Tool construction: `&mcp.Tool{Name, Description}` → `mcp.NewTool(name, mcp.WithDescription(desc))` with schema attached - - `mcp.ToolHandlerFor[In, Out]` type param → custom type alias (mcp-go has no generic handler type) - - `RESTHandler` closure stays (internal to our code, not SDK) - - `structSchema()` stays (used for REST bridge schema generation) +Add inline usage examples to field comments: -#### `pkg/mcp/mcp.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New imports:** `github.com/mark3labs/mcp-go/mcp` + `github.com/mark3labs/mcp-go/server` -- **Changes:** - - `Service.server` field: `*mcp.Server` → `*server.MCPServer` - - `New()`: replace `mcp.NewServer(impl, nil)` → `server.NewMCPServer("core-cli", "0.1.0", server.WithToolCapabilities(true))` - - `Server()` return type: `*mcp.Server` → `*server.MCPServer` - - `Run()`: replace `s.server.Run(ctx, &mcp.StdioTransport{})` → `server.ServeStdio(s.server)` - - `registerTools()` param: `server *mcp.Server` → `server *server.MCPServer` - - All `addToolRecorded(s, server, group, &mcp.Tool{...}, handler)` calls → updated tool construction - - All handler signatures: `*mcp.CallToolRequest` → `mcp.CallToolRequest` (pointer → value) +```go +// ReadFileInput contains parameters for reading a file. +// +// input := ReadFileInput{Path: "src/main.go"} +type ReadFileInput struct { + Path string `json:"path"` // e.g. "src/main.go" +} -### Phase 2 — Transports +// ReadFileOutput contains the result of reading a file. +type ReadFileOutput struct { + Content string `json:"content"` // File contents as string + Language string `json:"language"` // e.g. "go", "typescript" + Path string `json:"path"` // Echoed input path +} +``` -#### `pkg/mcp/transport_stdio.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New import:** `github.com/mark3labs/mcp-go/server` -- **Change:** `s.server.Run(ctx, &mcp.StdioTransport{})` → `server.ServeStdio(s.server)` -- **Note:** `ServeStdio` is a blocking function, same as `Run`. Context cancellation may need different handling (investigate mcp-go's stdio shutdown). +Apply the same pattern to all Input/Output types in `mcp.go`: +- `WriteFileInput/Output` +- `ListDirectoryInput/Output`, `DirectoryEntry` +- `CreateDirectoryInput/Output` +- `DeleteFileInput/Output` +- `RenameFileInput/Output` +- `FileExistsInput/Output` +- `DetectLanguageInput/Output` +- `GetSupportedLanguagesInput/Output` +- `EditDiffInput/Output` -#### `pkg/mcp/transport_http.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New import:** `github.com/mark3labs/mcp-go/server` -- **Changes:** - - Replace `mcp.NewStreamableHTTPHandler(factory, opts)` → `server.NewStreamableHTTPServer(s.server, opts...)` - - mcp-go's HTTP server may handle auth differently — investigate built-in auth options vs keeping our `withAuth` wrapper - - `StreamableHTTPOptions{SessionTimeout}` → check mcp-go equivalent options - - The factory function `func(r *http.Request) *mcp.Server` pattern may not exist — mcp-go likely uses a single server instance +- [ ] **Step 2: Update tool file comments** -#### `pkg/mcp/transport_tcp.go` -- **Old imports:** `github.com/modelcontextprotocol/go-sdk/jsonrpc` + `github.com/modelcontextprotocol/go-sdk/mcp` -- **New imports:** `github.com/mark3labs/mcp-go/server` (+ potentially mcp-go internals) -- **Changes:** - - **Critical:** `connTransport` implements `mcp.Transport` interface — no equivalent in mcp-go - - **Critical:** `connConnection` implements `mcp.Connection` with `Read`/`Write`/`Close`/`SessionID` — no equivalent - - `handleConnection()` creates per-connection `mcp.NewServer` + registers tools — must use mcp-go equivalent - - `jsonrpc.DecodeMessage`/`jsonrpc.EncodeMessage` — no public equivalent in mcp-go - - **Decision needed:** Replace TCP with HTTP transport, OR implement custom transport adapter - - Per-connection server instances: `mcp.NewServer()` → `server.NewMCPServer()` + re-register tools - - `mcp.Implementation{Name, Version}` struct literal → string args to `NewMCPServer` +For each tool file (`tools_metrics.go`, `tools_process.go`, `tools_rag.go`, `tools_webview.go`, `tools_ws.go`), add usage-example comments to: +- Input/Output struct definitions +- Handler function doc comments +- Registration function doc comments -#### `pkg/mcp/transport_unix.go` -- **No direct SDK import** — delegates to `handleConnection()` from `transport_tcp.go` -- **Impact:** Inherits whatever transport approach we choose for TCP -- **No changes** if we keep the `handleConnection()` pattern +Example pattern: -### Phase 3 — Tool files (mechanical changes) +```go +// ProcessStartInput contains parameters for starting a new process. +// +// input := ProcessStartInput{Command: "go", Args: []string{"test", "./..."}} +type ProcessStartInput struct { + Command string `json:"command"` // e.g. "go", "npm" + Args []string `json:"args,omitempty"` // e.g. ["test", "./..."] + Dir string `json:"dir,omitempty"` // Working directory, e.g. "/path/to/project" + Env []string `json:"env,omitempty"` // e.g. ["DEBUG=true", "PORT=8080"] +} +``` -All tool files follow the same pattern. For each: -1. Change import from `github.com/modelcontextprotocol/go-sdk/mcp` → `github.com/mark3labs/mcp-go/mcp` + `github.com/mark3labs/mcp-go/server` -2. Change handler param `*mcp.CallToolRequest` → `mcp.CallToolRequest` (pointer → value) -3. Change registration calls from `mcp.AddTool(server, &mcp.Tool{...}, handler)` → `server.AddTool(tool, adaptedHandler)` -4. Change `registerXTools(server *mcp.Server)` → `registerXTools(server *server.MCPServer)` +- [ ] **Step 3: Update registry.go comments** -#### `pkg/mcp/tools_metrics.go` -- `registerMetricsTools(server *mcp.Server)` → `registerMetricsTools(server *server.MCPServer)` -- 2 tool registrations: `metrics_record`, `metrics_query` -- Uses `mcp.AddTool` directly (not `addToolRecorded`) — change to `server.AddTool` +```go +// addToolRecorded registers a tool with the MCP server AND records its metadata +// for the REST bridge. The generic type parameters capture In/Out for schema extraction. +// +// addToolRecorded(s, server, "files", &mcp.Tool{ +// Name: "file_read", +// Description: "Read the contents of a file", +// }, s.readFile) +func addToolRecorded[In, Out any](...) { ... } +``` -#### `pkg/mcp/tools_process.go` -- `registerProcessTools(server *mcp.Server) bool` → `registerProcessTools(server *server.MCPServer) bool` -- 6 tool registrations: `process_start`, `process_stop`, `process_kill`, `process_list`, `process_output`, `process_input` -- Uses `mcp.AddTool` directly +- [ ] **Step 4: Update transport comments** -#### `pkg/mcp/tools_rag.go` -- `registerRAGTools(server *mcp.Server)` → `registerRAGTools(server *server.MCPServer)` -- 3 tool registrations: `rag_query`, `rag_ingest`, `rag_collections` -- Uses `mcp.AddTool` directly +```go +// ServeStdio starts the MCP server on stdin/stdout. +// This is the default transport for IDE integration. +// +// err := svc.ServeStdio(ctx) +func (s *Service) ServeStdio(ctx context.Context) error { ... } -#### `pkg/mcp/tools_webview.go` -- `registerWebviewTools(server *mcp.Server)` → `registerWebviewTools(server *server.MCPServer)` -- 10 tool registrations: `webview_connect` through `webview_wait` -- Uses `mcp.AddTool` directly +// ServeTCP starts the MCP server on a TCP address. +// Each connection gets its own MCP session. +// +// err := svc.ServeTCP(ctx, "127.0.0.1:9100") +func (s *Service) ServeTCP(ctx context.Context, addr string) error { ... } -#### `pkg/mcp/tools_ws.go` -- `registerWSTools(server *mcp.Server) bool` → `registerWSTools(server *server.MCPServer) bool` -- 2 tool registrations: `ws_start`, `ws_info` -- Uses `mcp.AddTool` directly +// ServeUnix starts the MCP server on a Unix domain socket. +// +// err := svc.ServeUnix(ctx, "/tmp/core-mcp.sock") +func (s *Service) ServeUnix(ctx context.Context, socketPath string) error { ... } -### Phase 4 — Subsystem packages +// ServeHTTP starts the MCP server with Streamable HTTP transport. +// Supports optional Bearer token auth via MCP_AUTH_TOKEN env var. +// +// err := svc.ServeHTTP(ctx, "127.0.0.1:9101") +func (s *Service) ServeHTTP(ctx context.Context, addr string) error { ... } +``` -#### `pkg/mcp/ide/ide.go` -- **Old import:** `github.com/modelcontextprotocol/go-sdk/mcp` -- **New imports:** `github.com/mark3labs/mcp-go/mcp` + `github.com/mark3labs/mcp-go/server` -- `RegisterTools(server *mcp.Server)` → `RegisterTools(server *server.MCPServer)` +- [ ] **Step 5: Verify compilation** -#### `pkg/mcp/ide/tools_build.go` -- `registerBuildTools(server *mcp.Server)` → `registerBuildTools(server *server.MCPServer)` -- 3 tools, handler signatures change `*mcp.CallToolRequest` → `mcp.CallToolRequest` +```bash +go vet ./pkg/mcp/... +go build ./pkg/mcp/... +``` -#### `pkg/mcp/ide/tools_chat.go` -- `registerChatTools(server *mcp.Server)` → `registerChatTools(server *server.MCPServer)` -- 5 tools, handler signatures change +- [ ] **Step 6: Commit** -#### `pkg/mcp/ide/tools_dashboard.go` -- `registerDashboardTools(server *mcp.Server)` → `registerDashboardTools(server *server.MCPServer)` -- 3 tools, handler signatures change +```bash +git add pkg/mcp/*.go +git commit -m "docs(mcp): add usage-example comments to all public types -#### `pkg/mcp/brain/brain.go` -- `RegisterTools(server *mcp.Server)` → `RegisterTools(server *server.MCPServer)` +CoreGO convention: comments show how to call the thing, not just +what it does. Adds inline examples to Input/Output structs, handler +functions, transport methods, and registry functions. -#### `pkg/mcp/brain/tools.go` -- `registerBrainTools(server *mcp.Server)` → `registerBrainTools(server *server.MCPServer)` -- 4 tools: `brain_remember`, `brain_recall`, `brain_forget`, `brain_list` -- Handler signatures change - -#### `pkg/mcp/brain/direct.go` -- `RegisterTools(server *mcp.Server)` → `RegisterTools(server *server.MCPServer)` -- 3 tools: `brain_remember`, `brain_recall`, `brain_forget` -- Handler signatures change - -### Phase 5 — Agentic subsystem - -#### `pkg/mcp/agentic/prep.go` -- `RegisterTools(server *mcp.Server)` → `RegisterTools(server *server.MCPServer)` -- Registers `agentic_prep_workspace` + `agentic_scan` + delegates to sub-registration functions -- Handler signatures change - -#### `pkg/mcp/agentic/dispatch.go` -- `registerDispatchTool(server *mcp.Server)` → `registerDispatchTool(server *server.MCPServer)` -- 1 tool: `agentic_dispatch` - -#### `pkg/mcp/agentic/status.go` -- `registerStatusTool(server *mcp.Server)` → `registerStatusTool(server *server.MCPServer)` -- 1 tool: `agentic_status` - -#### `pkg/mcp/agentic/scan.go` -- Handler signature change only (registration is in `prep.go`) - -#### `pkg/mcp/agentic/resume.go` -- `registerResumeTool(server *mcp.Server)` → `registerResumeTool(server *server.MCPServer)` -- 1 tool: `agentic_resume` - -#### `pkg/mcp/agentic/plan.go` -- `registerPlanTools(server *mcp.Server)` → `registerPlanTools(server *server.MCPServer)` -- 5 tools: `agentic_plan_create`, `agentic_plan_read`, `agentic_plan_update`, `agentic_plan_delete`, `agentic_plan_list` - -#### `pkg/mcp/agentic/pr.go` -- `registerCreatePRTool(server *mcp.Server)` → `registerCreatePRTool(server *server.MCPServer)` -- `registerListPRsTool(server *mcp.Server)` → `registerListPRsTool(server *server.MCPServer)` -- 2 tools: `agentic_create_pr`, `agentic_list_prs` - -#### `pkg/mcp/agentic/epic.go` -- `registerEpicTool(server *mcp.Server)` → `registerEpicTool(server *server.MCPServer)` -- 1 tool: `agentic_create_epic` - -### Phase 6 — Bridge (no SDK changes) - -#### `pkg/mcp/bridge.go` -- **No MCP SDK import** — uses `gin` and `api` only -- **No changes needed** — the REST bridge consumes `ToolRecord` which is our own type - -### Phase 7 — Tests - -All test files that import the SDK need the same import swap and type changes. Key test files: -- `pkg/mcp/subsystem_test.go` — references `*mcp.Server` -- `pkg/mcp/registry_test.go` — tests `addToolRecorded` -- `pkg/mcp/mcp_test.go` — creates `Service` -- `pkg/mcp/bridge_test.go` — tests REST bridge -- `pkg/mcp/transport_tcp_test.go` — tests TCP transport -- `pkg/mcp/transport_e2e_test.go` — end-to-end transport tests -- `pkg/mcp/tools_*_test.go` — tool handler tests -- `pkg/mcp/ide/bridge_test.go`, `pkg/mcp/ide/tools_test.go` -- `pkg/mcp/brain/brain_test.go` +Co-Authored-By: Virgil " +``` --- -## 4. Breaking Changes & Risks +## Phase 4: Wire Notifications into Subsystems -### No direct equivalent +Connect the notification system to existing subsystems so they emit channel events. -| Feature | Official SDK | mcp-go | Mitigation | -|---------|-------------|--------|------------| -| Generic typed handlers | `ToolHandlerFor[In, Out]` | None — untyped `ToolHandler` | Write `adaptHandler[In, Out]()` adapter (section 2a) | -| Auto input schema from structs | Via `addToolRecorded` reflection | Must declare or supply raw schema | Keep `structSchema()` + attach via raw schema option (section 2b) | -| TCP transport interfaces | `mcp.Transport`, `mcp.Connection` | Not exposed | Replace TCP with HTTP, or implement adapter (section 2d) | -| JSON-RPC codec | `jsonrpc.DecodeMessage`/`EncodeMessage` | Not exposed | Only needed for TCP — goes away if TCP is replaced | -| Per-connection server instances | `mcp.NewServer()` per TCP conn | Single `MCPServer` with sessions | Use mcp-go's session model (section 2d) | -| `*mcp.CallToolRequest` (pointer) | Used in all handlers | `mcp.CallToolRequest` (value) | Mechanical change in all handler signatures | +**Files:** +- Modify: `pkg/mcp/subsystem.go` (add Notifier interface, SubsystemWithNotifier) +- Modify: `pkg/mcp/mcp.go` (call SetNotifier in New()) +- Modify: `pkg/mcp/tools_process.go` (emit process lifecycle events) +- Modify: `pkg/mcp/brain/brain.go` (accept Notifier, emit brain events from bridge callback) -### Consumer impact +- [ ] **Step 1: Define Notifier interface to avoid circular imports** -The `Subsystem` interface change (`*mcp.Server` → `*server.MCPServer`) breaks: -- `agent` module — must update its subsystem implementations -- `ide` module — must update its subsystem implementations +Sub-packages (`brain/`, `ide/`) cannot import `pkg/mcp` without creating a cycle. Define a small `Notifier` interface that sub-packages can accept without importing the parent package: -**Mitigation:** Coordinate the migration. Update `forge.lthn.ai/core/mcp` first, then update consumers to match. +```go +// Notifier pushes events to connected MCP sessions. +// Implemented by *Service. Sub-packages accept this interface +// to avoid circular imports. +// +// notifier.ChannelSend(ctx, "build.complete", data) +type Notifier interface { + ChannelSend(ctx context.Context, channel string, data any) +} +``` -### New capabilities unlocked +Add an optional `SubsystemWithNotifier` interface: -After migration, the following become possible: -- `server.ServerFromContext(ctx)` — access server from any tool handler -- `SendNotificationToClient(ctx, "claude/channel", payload)` — push events to Claude Code -- `SendNotificationToAllClients("claude/channel", payload)` — broadcast to all sessions -- Session hooks for connection tracking and cleanup -- Per-session tool registration (different tools for different clients) +```go +// SubsystemWithNotifier extends Subsystem for those that emit channel events. +// SetNotifier is called after New() before any tool calls. +type SubsystemWithNotifier interface { + Subsystem + SetNotifier(n Notifier) +} +``` + +In `New()`, after creating the service: + +```go +for _, sub := range s.subsystems { + sub.RegisterTools(s.server) + if sn, ok := sub.(SubsystemWithNotifier); ok { + sn.SetNotifier(s) + } +} +``` + +- [ ] **Step 2: Emit process lifecycle events** + +Process tools live in `pkg/mcp/` (same package as Service), so they can call `s.ChannelSend` directly: + +```go +// After successful process start: +s.ChannelSend(ctx, "process.start", map[string]any{ + "id": output.ID, + "command": input.Command, +}) + +// In the process exit callback (if wired via ProcessEventCallback): +s.ChannelSend(ctx, "process.exit", map[string]any{ + "id": id, + "exitCode": code, +}) +``` + +- [ ] **Step 3: Emit brain events in brain subsystem** + +The brain subsystem's recall handler sends requests to the Laravel bridge asynchronously — the returned `output` does not contain real results (they arrive via WebSocket later). Instead, emit the notification from the bridge callback where results actually arrive. + +In `pkg/mcp/brain/brain.go`, add a `Notifier` field and `SetNotifier` method: + +```go +type Subsystem struct { + bridge *ide.Bridge + notifier Notifier // set by SubsystemWithNotifier +} + +// Notifier pushes events to MCP sessions (matches pkg/mcp.Notifier). +type Notifier interface { + ChannelSend(ctx context.Context, channel string, data any) +} + +func (s *Subsystem) SetNotifier(n Notifier) { + s.notifier = n +} +``` + +Then in the bridge message handler (where recall results are received from Laravel), emit the notification with the actual result count: + +```go +// In the bridge callback that processes recall results: +if s.notifier != nil { + s.notifier.ChannelSend(ctx, "brain.recall.complete", map[string]any{ + "query": query, + "count": len(memories), + }) +} +``` + +- [ ] **Step 4: Verify compilation and tests** + +```bash +go vet ./pkg/mcp/... +go test ./pkg/mcp/... +``` + +- [ ] **Step 5: Commit** + +```bash +git add pkg/mcp/subsystem.go pkg/mcp/mcp.go pkg/mcp/tools_process.go pkg/mcp/brain/brain.go +git commit -m "feat(mcp): wire channel notifications into process and brain subsystems + +Adds Notifier interface to avoid circular imports between pkg/mcp +and sub-packages. Subsystems that implement SubsystemWithNotifier +receive a Notifier reference. Process tools emit process.start and +process.exit channel events. Brain subsystem emits +brain.recall.complete from the bridge callback (not the handler +return, which is async). + +Co-Authored-By: Virgil " +``` --- -## 5. Migration Order (Recommended) +## Phase 5: Consumer Migration Guide -1. **Phase 1:** Core types (`subsystem.go`, `registry.go`, `mcp.go`) — establishes the foundation -2. **Phase 2:** Transports (`transport_stdio.go`, `transport_http.go`, `transport_tcp.go`) — the riskiest phase -3. **Phase 3:** Tool files (mechanical, low risk) — `tools_metrics.go`, `tools_process.go`, `tools_rag.go`, `tools_webview.go`, `tools_ws.go` -4. **Phase 4:** IDE subsystem (`ide/`) -5. **Phase 5:** Brain subsystem (`brain/`) -6. **Phase 6:** Agentic subsystem (`agentic/`) -7. **Phase 7:** Tests — update in parallel with each phase -8. **Phase 8:** Consumer modules (`agent`, `ide`) — update after core module is published +Document the breaking changes for the 2 consumers (agent, ide modules). -Each phase should be a separate commit. Build must pass after each phase. +**Files:** +- Create: `docs/migration-guide-options.md` + +- [ ] **Step 1: Write migration guide** + +```markdown +# Migrating to Options{} Constructor + +## Before (functional options) + + svc, err := mcp.New( + mcp.WithWorkspaceRoot("/path"), + mcp.WithProcessService(ps), + mcp.WithWSHub(hub), + mcp.WithSubsystem(brainSub), + mcp.WithSubsystem(ideSub), + ) + +## After (Options struct) + + svc, err := mcp.New(mcp.Options{ + WorkspaceRoot: "/path", + ProcessService: ps, + WSHub: hub, + Subsystems: []mcp.Subsystem{brainSub, ideSub}, + }) + +## New notification API + + // Broadcast to all sessions (LoggingLevel is a string type) + svc.SendNotificationToAllClients(ctx, "info", "build", data) + + // Push a named channel event + svc.ChannelSend(ctx, "build.complete", data) + + // Push to a specific session + for session := range svc.Sessions() { + svc.ChannelSendToSession(ctx, session, "agent.status", data) + } +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/migration-guide-options.md +git commit -m "docs(mcp): add migration guide for Options{} constructor + +Documents breaking changes from functional options to Options{} +struct for consumers (agent, ide modules). Includes notification +API examples. + +Co-Authored-By: Virgil " +``` --- -## 6. Estimated Scope +## Summary -| Category | Files | Tools | -|----------|-------|-------| -| Core (mcp.go, registry.go, subsystem.go) | 3 | — | -| Transports | 4 | — | -| Tool files (pkg/mcp/) | 5 | 23 tools | -| IDE subsystem | 4 | 11 tools | -| Brain subsystem | 3 | 7 tools | -| Agentic subsystem | 8 | 14 tools | -| Tests | ~12 | — | -| **Total** | **~39 files** | **55 tools** | +**Total: 5 phases, 24 steps** ---- +| Phase | Scope | Breaking? | +|-------|-------|-----------| +| 1 | `Options{}` struct replaces functional options | Yes — 2 consumers | +| 2 | Notification broadcasting + claude/channel | No — new API | +| 3 | Usage-example comments | No — docs only | +| 4 | Wire notifications into subsystems | No — additive | +| 5 | Consumer migration guide | No — docs only | -## 7. Checklist - -- [ ] Verify `mcp-go` latest version supports `InputSchema` raw attachment -- [ ] Confirm `mcp-go`'s `CallToolRequest` field layout matches our assumptions -- [ ] Investigate mcp-go's stdio shutdown/context cancellation behaviour -- [ ] Decide: TCP transport → HTTP replacement or custom adapter -- [ ] Investigate mcp-go's HTTP server auth options (keep `withAuth` or use built-in) -- [ ] Write `adaptHandler[In, Out]()` generic adapter -- [ ] Write tool schema attachment helper (raw JSON Schema → `mcp.NewTool`) -- [ ] Update `addToolRecorded` to use new registration API -- [ ] Migrate all 55 tool registrations -- [ ] Update all handler signatures (`*mcp.CallToolRequest` → `mcp.CallToolRequest`) -- [ ] Update `Subsystem` interface + all implementations -- [ ] Update consumer modules (`agent`, `ide`) -- [ ] Run full test suite -- [ ] Add notification support (the whole point of the migration) +After completion: +- `mcp.New(mcp.Options{...})` replaces `mcp.New(mcp.WithXxx(...))` +- `svc.SendNotificationToAllClients(ctx, level, logger, data)` broadcasts to all sessions +- `svc.ChannelSend(ctx, "build.complete", data)` pushes named events +- `claude/channel` experimental capability advertised during MCP initialisation +- Clients (Claude Code, IDEs) can discover push-event support and receive real-time updates +- All public types and functions have usage-example comments diff --git a/go.mod b/go.mod index e231f69..8960f2b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module forge.lthn.ai/core/mcp go 1.26.0 require ( + dappco.re/go/core v0.4.7 forge.lthn.ai/core/api v0.1.5 forge.lthn.ai/core/cli v0.3.7 - forge.lthn.ai/core/go v0.3.3 forge.lthn.ai/core/go-ai v0.1.12 forge.lthn.ai/core/go-io v0.1.7 forge.lthn.ai/core/go-log v0.0.4 @@ -21,6 +21,7 @@ require ( ) require ( + forge.lthn.ai/core/go v0.3.3 // indirect forge.lthn.ai/core/go-i18n v0.1.7 // indirect forge.lthn.ai/core/go-inference v0.1.6 // indirect github.com/99designs/gqlgen v0.17.88 // indirect diff --git a/go.sum b/go.sum index cba2eb1..77b90fe 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA= +dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= forge.lthn.ai/core/api v0.1.5 h1:NwZrcOyBjaiz5/cn0n0tnlMUodi8Or6FHMx59C7Kv2o= forge.lthn.ai/core/api v0.1.5/go.mod h1:PBnaWyOVXSOGy+0x2XAPUFMYJxQ2CNhppia/D06ZPII= forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= diff --git a/pkg/mcp/tools_process_ci_test.go b/pkg/mcp/tools_process_ci_test.go index 7bbbca7..f73314e 100644 --- a/pkg/mcp/tools_process_ci_test.go +++ b/pkg/mcp/tools_process_ci_test.go @@ -8,34 +8,36 @@ import ( "testing" "time" + "dappco.re/go/core" "forge.lthn.ai/core/go-process" - core "forge.lthn.ai/core/go/pkg/core" ) // newTestProcessService creates a real process.Service backed by a core.Core for CI tests. func newTestProcessService(t *testing.T) *process.Service { t.Helper() - c, err := core.New( - core.WithName("process", process.NewService(process.Options{})), - ) + c := core.New() + raw, err := process.NewService(process.Options{})(c) if err != nil { t.Fatalf("Failed to create process service: %v", err) } + svc := raw.(*process.Service) - svc, err := core.ServiceFor[*process.Service](c, "process") - if err != nil { - t.Fatalf("Failed to get process service: %v", err) + resultFrom := func(err error) core.Result { + if err != nil { + return core.Result{Value: err} + } + return core.Result{OK: true} } - - if err := svc.OnStartup(context.Background()); err != nil { - t.Fatalf("Failed to start process service: %v", err) - } - t.Cleanup(func() { - _ = svc.OnShutdown(context.Background()) - core.ClearInstance() + c.Service("process", core.Service{ + OnStart: func() core.Result { return resultFrom(svc.OnStartup(context.Background())) }, + OnStop: func() core.Result { return resultFrom(svc.OnShutdown(context.Background())) }, }) + if r := c.ServiceStartup(context.Background(), nil); !r.OK { + t.Fatalf("Failed to start core: %v", r.Value) + } + t.Cleanup(func() { c.ServiceShutdown(context.Background()) }) return svc }