From 39f3df85b8691564015f1e6644ba8668f15d2bf7 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Fri, 6 Nov 2026 23:29:34 +0100 Subject: [PATCH 1/2] sdd: wip start 20261106-232735-d-tac-d3f (Christopher) SDD-Mutation: v1:8d142be58e9c9fdb910fdf34010cad39a3c4214bac4a2769a61caf278f5d9e2c --- .sdd/graph/wip/20261106-232933-christopher.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .sdd/graph/wip/20261106-232933-christopher.md diff --git a/.sdd/graph/wip/20261106-232933-christopher.md b/.sdd/graph/wip/20261106-232933-christopher.md new file mode 100644 index 00000000..ff9f8f48 --- /dev/null +++ b/.sdd/graph/wip/20261106-232933-christopher.md @@ -0,0 +1,7 @@ +--- +entry: 20261106-232735-d-tac-d3f +participant: Christopher +exclusive: true +--- + +mcpapp registers SDD's tools onto a host-supplied MCP server (d-tac-d3f) From 298914393c398378104af169995ed53af156b8cf Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Fri, 6 Nov 2026 23:33:29 +0100 Subject: [PATCH 2/2] feat(mcpapp): register SDD's tools onto a host-supplied server Register adds SDD's tools to an mcp.Server the host constructs, beside the host's own tools; Instructions exports the server instructions text. New stays a wrapper that builds a server named sdd and registers onto it. Implements 20261106-232735-d-tac-d3f. Co-Authored-By: Claude Opus 5.5 --- examples/extendingsdd/README.md | 2 +- pkg/mcpapp/doc.go | 8 +- pkg/mcpapp/instructions.go | 4 +- pkg/mcpapp/register_test.go | 82 ++++++++++++++++++ pkg/mcpapp/request_identity.go | 2 +- pkg/mcpapp/server.go | 83 ++++++++++++++----- pkg/mcpapp/server_test.go | 3 +- ...down_test.go => shutdown_internal_test.go} | 2 +- pkg/mcpapp/tools.go | 76 ++++++++--------- 9 files changed, 193 insertions(+), 69 deletions(-) create mode 100644 pkg/mcpapp/register_test.go rename pkg/mcpapp/{shutdown_test.go => shutdown_internal_test.go} (98%) diff --git a/examples/extendingsdd/README.md b/examples/extendingsdd/README.md index dfb36e15..ccf9649c 100644 --- a/examples/extendingsdd/README.md +++ b/examples/extendingsdd/README.md @@ -9,7 +9,7 @@ The composition in `main.go` owns: - graph, LLM, embedding/index, and finalizer adapters per project, and one session store and one staged-blob store for the whole composition; - the HTTP listener and deployment policy. -SDD owns graph semantics, workflow procedures, read rendering, pre-flight and summary behavior, durable transition recovery, and write gates. The composition imports the canonical `application` contracts and optional `local` adapters, constructs an `application.ProjectRuntime` per project, resolves them through `application.AccessResolver`, creates `application.Application` with the composition-wide session and staged-blob stores, and mounts `mcpapp.Server.Handler()` behind the MCP SDK's bearer middleware. A session handle is the session ID alone; the application reads a session's home project from its own record, so the server pins no project and serves every project the principal can reach. +SDD owns graph semantics, workflow procedures, read rendering, pre-flight and summary behavior, durable transition recovery, and write gates. The composition imports the canonical `application` contracts and optional `local` adapters, constructs an `application.ProjectRuntime` per project, resolves them through `application.AccessResolver`, creates `application.Application` with the composition-wide session and staged-blob stores, and mounts `mcpapp.Server.Handler()` behind the MCP SDK's bearer middleware. A session handle is the session ID alone; the application reads a session's home project from its own record, so the server pins no project and serves every project the principal can reach. A composition with MCP tools of its own constructs the server itself and adds SDD's tools to it with `mcpapp.Register`. For local development the module uses `replace github.com/networkteam/sdd => ../..`. A release consumer removes that replacement and pins a published SDD version. diff --git a/pkg/mcpapp/doc.go b/pkg/mcpapp/doc.go index d02833c3..45da2c0a 100644 --- a/pkg/mcpapp/doc.go +++ b/pkg/mcpapp/doc.go @@ -1,8 +1,10 @@ // Package mcpapp adapts the protocol-neutral SDD application to MCP. // -// New registers the shared tool surface over an application.Application. Handler -// exposes stateless Streamable HTTP so an embedding composition can -// place its own authentication middleware in front. Authentication middleware +// Register adds the shared tool surface over an application.Application to an +// MCP server the host constructs, beside tools of the host's own. New builds a +// server carrying SDD's tools alone; its Handler exposes stateless Streamable +// HTTP so an embedding composition can place its own authentication middleware +// in front. Authentication middleware // must populate the MCP SDK's TokenInfo on every request; mcpapp translates // that current request identity into application.RequestIdentity and never treats the // session's initialization identity as authorization proof. diff --git a/pkg/mcpapp/instructions.go b/pkg/mcpapp/instructions.go index 62413f2b..bc68cff3 100644 --- a/pkg/mcpapp/instructions.go +++ b/pkg/mcpapp/instructions.go @@ -7,7 +7,9 @@ package mcpapp // short and front-loads what matters. Tier one is the session shell's opening // serve (the user-dialogue base procedure); tier two is the per-step units. -const serverInstructions = `This server hosts an SDD (Signal-Dialogue-Decision) graph: an append-only +// Instructions is SDD's server instructions text. A host that registers SDD's +// tools with [Register] includes it in its own server instructions. +const Instructions = `This server hosts an SDD (Signal-Dialogue-Decision) graph: an append-only record of project observations (signals) and commitments (decisions) grown through dialogue between the user and their agents. diff --git a/pkg/mcpapp/register_test.go b/pkg/mcpapp/register_test.go new file mode 100644 index 00000000..14c6e013 --- /dev/null +++ b/pkg/mcpapp/register_test.go @@ -0,0 +1,82 @@ +package mcpapp_test + +import ( + "context" + "slices" + "strings" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + mcpserver "github.com/networkteam/sdd/pkg/mcpapp" +) + +type echoArgs struct { + Text string `json:"text"` +} + +type echoResult struct { + Text string `json:"text"` +} + +// TestRegisterServesSDDToolsBesideHostTools composes a host-supplied server +// carrying SDD's tools and a host tool, and drives both through one +// connection (d-tac-d3f). +func TestRegisterServesSDDToolsBesideHostTools(t *testing.T) { + env := newTestServer(t, nil, "", "") + host := mcp.NewServer(&mcp.Implementation{Name: "host", Version: "test"}, &mcp.ServerOptions{ + Instructions: mcpserver.Instructions + "\n\nhost_echo echoes text.", + }) + if err := mcpserver.Register(host, env.opts); err != nil { + t.Fatal(err) + } + mcp.AddTool(host, &mcp.Tool{Name: "host_echo", Description: "Echo text."}, + func(_ context.Context, _ *mcp.CallToolRequest, in echoArgs) (*mcp.CallToolResult, echoResult, error) { + return nil, echoResult(in), nil + }) + serverTransport, clientTransport := mcp.NewInMemoryTransports() + if _, err := host.Connect(t.Context(), serverTransport, nil); err != nil { + t.Fatal(err) + } + cs := connect(t, nil, clientTransport) + + if got := cs.InitializeResult().ServerInfo.Name; got != "host" { + t.Errorf("server name = %q, want the host's", got) + } + if !strings.HasPrefix(cs.InitializeResult().Instructions, mcpserver.Instructions) { + t.Error("host instructions should carry SDD's instructions text") + } + want := append(toolNames(t, connect(t, env.srv)), "host_echo") + slices.Sort(want) + if got := toolNames(t, cs); !slices.Equal(got, want) { + t.Errorf("tools = %v, want SDD's tools plus host_echo %v", got, want) + } + + openSession(t, cs) + var echo echoResult + call(t, cs, "host_echo", map[string]any{"text": "hello"}, &echo) + if echo.Text != "hello" { + t.Errorf("host_echo = %q, want hello", echo.Text) + } +} + +func TestRegisterRequiresServer(t *testing.T) { + env := newTestServer(t, nil, "", "") + if err := mcpserver.Register(nil, env.opts); err == nil { + t.Fatal("Register without a server should fail") + } +} + +func toolNames(t *testing.T, cs *mcp.ClientSession) []string { + t.Helper() + res, err := cs.ListTools(t.Context(), &mcp.ListToolsParams{}) + if err != nil { + t.Fatal(err) + } + names := make([]string, 0, len(res.Tools)) + for _, tool := range res.Tools { + names = append(names, tool.Name) + } + slices.Sort(names) + return names +} diff --git a/pkg/mcpapp/request_identity.go b/pkg/mcpapp/request_identity.go index 157e4016..4e9a8863 100644 --- a/pkg/mcpapp/request_identity.go +++ b/pkg/mcpapp/request_identity.go @@ -37,7 +37,7 @@ func publicIdentityFromRequest(req *mcp.CallToolRequest) sdd.RequestIdentity { return sdd.RequestIdentity{Subject: identity.Subject, Scopes: identity.Scopes, Attributes: identity.Attributes} } -func (s *Server) requestIdentity(req *mcp.CallToolRequest) sdd.RequestIdentity { +func (s *toolSet) requestIdentity(req *mcp.CallToolRequest) sdd.RequestIdentity { identity := publicIdentityFromRequest(req) if identity.Subject == "" && s.localIdentity.Subject != "" { return s.localIdentity diff --git a/pkg/mcpapp/server.go b/pkg/mcpapp/server.go index d76fadde..3880f1ee 100644 --- a/pkg/mcpapp/server.go +++ b/pkg/mcpapp/server.go @@ -12,7 +12,7 @@ import ( sdd "github.com/networkteam/sdd/pkg/application" ) -// Options configures a workflow MCP server. +// Options configures SDD's workflow tools. type Options struct { // Application is the protocol-neutral SDD runtime. The server pins no // project: start_session selects one inside the dialogue, a sole @@ -30,13 +30,14 @@ type Options struct { // (stdio transport). Local clients get absolute paths in read results // (read_attachment) so they can read files directly instead of paging. LocalClient bool - Version string + // Version is the SDD version the info tool reports, and the server + // implementation version under New. + Version string } -// Server wires the MCP protocol surface to the engine and the SDD read and -// write layers. -type Server struct { - mcp *mcp.Server +// toolSet carries what SDD's tool handlers need, independent of the server +// they are registered on. +type toolSet struct { app *sdd.Application searchSyncMode sdd.SearchSyncMode localIdentity sdd.RequestIdentity @@ -46,39 +47,75 @@ type Server struct { // about a dialogue: nothing is keyed to a connection, and no connection // event acts on a session (d-cpt-aen). sessions *sessionCache - - lifecycleMu sync.Mutex - closing bool } -// ErrServerClosing reports that a connection or request arrived after -// graceful shutdown began. -var ErrServerClosing = errors.New("mcpapp: server is shutting down") - -// New constructs the server and registers the workflow tool surface. -func New(opts Options) (*Server, error) { +func newToolSet(opts Options) (*toolSet, error) { if !opts.SearchSyncMode.Valid() { return nil, errors.New("mcpapp: SearchSyncMode is required: none, local, or all") } if opts.Application == nil { return nil, errors.New("mcpapp: Application is required") } - s := &Server{ + return &toolSet{ app: opts.Application, searchSyncMode: opts.SearchSyncMode, localIdentity: opts.LocalIdentity, local: opts.LocalClient, version: opts.Version, sessions: newSessionCache(), + }, nil +} + +// Register adds SDD's workflow tools to a server the host constructs, beside +// any tools of the host's own (d-tac-d3f). The host owns that server: its +// implementation name and version, its instructions, composed around +// [Instructions], its transport and handler, its authentication, and its +// lifecycle. SDD's tools derive nothing from the connection, so any transport +// serves them. +// +// The MCP SDK replaces a tool registered under an existing name without error: +// a host tool must not reuse one of SDD's tool names. +func Register(server *mcp.Server, opts Options) error { + if server == nil { + return errors.New("mcpapp: server is required") + } + tools, err := newToolSet(opts) + if err != nil { + return err + } + tools.register(server) + return nil +} + +// Server is an MCP server carrying SDD's tools alone, for compositions that +// add no tools of their own. It owns the transport and the lifecycle. +type Server struct { + mcp *mcp.Server + + lifecycleMu sync.Mutex + closing bool +} + +// ErrServerClosing reports that a connection or request arrived after +// graceful shutdown began. +var ErrServerClosing = errors.New("mcpapp: server is shutting down") + +// New constructs a server named sdd and registers SDD's tools on it. +func New(opts Options) (*Server, error) { + tools, err := newToolSet(opts) + if err != nil { + return nil, err + } + s := &Server{ + mcp: mcp.NewServer(&mcp.Implementation{ + Name: "sdd", + Version: opts.Version, + }, &mcp.ServerOptions{ + Instructions: Instructions, + }), } - s.mcp = mcp.NewServer(&mcp.Implementation{ - Name: "sdd", - Version: opts.Version, - }, &mcp.ServerOptions{ - Instructions: serverInstructions, - }) s.mcp.AddReceivingMiddleware(s.refuseWhileClosing) - s.registerTools() + tools.register(s.mcp) return s, nil } diff --git a/pkg/mcpapp/server_test.go b/pkg/mcpapp/server_test.go index 9c3af449..672941af 100644 --- a/pkg/mcpapp/server_test.go +++ b/pkg/mcpapp/server_test.go @@ -261,6 +261,7 @@ func writeFixtureGraph(t *testing.T) string { type testEnv struct { srv *mcpserver.Server + opts mcpserver.Options graphDir string sessionsDir string sessions sdd.SessionStore @@ -414,7 +415,7 @@ func newTestServerConfig(t *testing.T, findings []query.Finding, graphDir, sessi t.Errorf("shutting down MCP server: %v", err) } }) - return testEnv{srv: srv, graphDir: graphDir, sessionsDir: sessionsDir, sessions: opts.Application.Sessions, targets: targets} + return testEnv{srv: srv, opts: opts.MCP, graphDir: graphDir, sessionsDir: sessionsDir, sessions: opts.Application.Sessions, targets: targets} } // connect uses an in-memory connection unless the caller supplies a transport. diff --git a/pkg/mcpapp/shutdown_test.go b/pkg/mcpapp/shutdown_internal_test.go similarity index 98% rename from pkg/mcpapp/shutdown_test.go rename to pkg/mcpapp/shutdown_internal_test.go index 40a90f5d..f2dd1da4 100644 --- a/pkg/mcpapp/shutdown_test.go +++ b/pkg/mcpapp/shutdown_internal_test.go @@ -93,7 +93,7 @@ func TestShutdownContextBoundsInFlightSessionDrain(t *testing.T) { } func newLifecycleTestServer() *Server { - server := &Server{sessions: newSessionCache()} + server := &Server{} server.mcp = mcp.NewServer(&mcp.Implementation{Name: "lifecycle-test", Version: "test"}, nil) server.mcp.AddReceivingMiddleware(server.refuseWhileClosing) return server diff --git a/pkg/mcpapp/tools.go b/pkg/mcpapp/tools.go index cdb55e27..0abd7aef 100644 --- a/pkg/mcpapp/tools.go +++ b/pkg/mcpapp/tools.go @@ -358,8 +358,8 @@ type RegistryResult struct { Functions []RegistryFuncResult `json:"functions"` } -func (s *Server) registerTools() { - mcp.AddTool(s.mcp, &mcp.Tool{ +func (s *toolSet) register(srv *mcp.Server) { + mcp.AddTool(srv, &mcp.Tool{ Name: "start_session", Description: "Open a fresh dialogue session — one of the two doors, and the only tool that takes a " + "project. Auto-starts the session shell (user-dialogue by default) and returns its opening " + @@ -371,7 +371,7 @@ func (s *Server) registerTools() { "dialogue under a new handle; to re-serve an existing one, present its handle to resume_session.", }, toolBoundary(s.startSession)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "start_procedure", Description: "Start a procedure instance (a playbook move such as capture) in the session named by " + "session (required). Returns the current step's instructions, the report schema to answer " + @@ -379,7 +379,7 @@ func (s *Server) registerTools() { "writes happen inside procedure transitions, never through a direct tool.", }, toolBoundary(s.startProcedure)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "next", Description: "Advance a procedure instance in the session named by session (required): send state " + "fields per the served report_schema, or answer a pending chooser with {chooser, choice, " + @@ -391,7 +391,7 @@ func (s *Server) registerTools() { "dialogue lands.", }, toolBoundary(s.next)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "abandon", Description: "Abandon a running move instance (instance, plus session naming the session it belongs " + "to), or tear down a session directly by handle (session alone) — one call, no resume, no " + @@ -400,7 +400,7 @@ func (s *Server) registerTools() { "session shell concludes through its own junction, never through abandon.", }, toolBoundary(s.abandon)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "park", Description: "Park a running move back to the session junction (session required): state and step " + "position keep, the move lists as an open thread (at junctions and on conclude), and next " + @@ -408,7 +408,7 @@ func (s *Server) registerTools() { "graph-visible thread instead of living in conversation memory as an agent promise.", }, toolBoundary(s.park)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "resume_session", Description: "The second door, and the compaction escape: re-serve the current position of the " + "session whose handle you present — its framing plus every running move at its current step, " + @@ -419,14 +419,14 @@ func (s *Server) registerTools() { "another conversation's context. A lost handle is the user's to recover, not yours to guess.", }, toolBoundary(s.resumeSession)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "bind_branch", Description: "Declare or clear the attached session's durable branch binding. Pass exactly one of " + "branch or clear:true. Setting validates the live registered checkout before changing the " + "session; clearing needs no branch capability.", }, toolBoundary(s.bindBranch)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "stage_attachment", Description: "Stage a file into the session scratch (session required) and get back a handle to pass " + "in a report's attachments field, or amend what is already staged: with patches, the file named " + @@ -434,46 +434,46 @@ func (s *Server) registerTools() { "Never a graph write — the write gate materializes staged files with the entry.", }, toolBoundary(s.stageAttachment)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "search", Description: "Search graph entries: terms (text/regex), query (semantic phrase), or both (hybrid). " + "A free read within the session named by session (required): no move needed, never blocked " + "by procedure state; it runs in that session's project and branch.", }, toolBoundary(s.search)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "view", Description: "Run an sdd view layout pipeline over the graph — overview sections, topic counts, " + "ranked lists. A free read within the session named by session (required).", }, toolBoundary(s.view)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "show", Description: "Read entries in full with their upstream and downstream reference chains, within the " + "session named by session (required). Use whenever the dialogue touches a specific entry — " + "summaries are pointers, not facts.", }, toolBoundary(s.show)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "read_attachment", Description: "Read an attachment's content, paged, within the session named by session (required): " + "an entry's by ID and filename, or a file staged in the session (before any entry carries it) " + "by handle. Never derive storage paths.", }, toolBoundary(s.readAttachment)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "info", Description: "Session framing header for the session named by session (required): project, local participant, configured language, and available search modes.", }, toolBoundary(s.info)) - mcp.AddTool(s.mcp, &mcp.Tool{ + mcp.AddTool(srv, &mcp.Tool{ Name: "registry", Description: "Engine function contracts (predicates, queries, commands) — what procedure spec authors consult. Carries the session handle like every other tool.", }, toolBoundary(s.registryDocs)) } // attachedSession returns the named session locked and replayed from its ledger. -func (s *Server) attachedSession(ctx context.Context, req *mcp.CallToolRequest, session string) (*shellSession, error) { +func (s *toolSet) attachedSession(ctx context.Context, req *mcp.CallToolRequest, session string) (*shellSession, error) { id := sdd.SessionID(strings.TrimSpace(session)) if id == "" { return nil, noHandleError() @@ -538,7 +538,7 @@ func noHandleError() error { // startSession opens a fresh dialogue: a new durable session under a new // handle, every call. Nothing is derived from the connection. -func (s *Server) startSession(ctx context.Context, req *mcp.CallToolRequest, args StartSessionArgs) (*mcp.CallToolResult, ServeResult, error) { +func (s *toolSet) startSession(ctx context.Context, req *mcp.CallToolRequest, args StartSessionArgs) (*mcp.CallToolResult, ServeResult, error) { identity := s.requestIdentity(req) // The door is the one tool that takes a project. Left empty, the // application infers a sole accessible project or reports the ambiguity, @@ -566,7 +566,7 @@ func (s *Server) startSession(ctx context.Context, req *mcp.CallToolRequest, arg // serveProjects is start_session's answer when the principal reaches several // projects and named none: no session opens, the projects are listed and the // caller picks one. -func (s *Server) serveProjects(ctx context.Context, identity sdd.RequestIdentity) (ServeResult, error) { +func (s *toolSet) serveProjects(ctx context.Context, identity sdd.RequestIdentity) (ServeResult, error) { list, err := s.app.Projects(ctx, identity) if err != nil { return ServeResult{}, err @@ -587,7 +587,7 @@ func (s *Server) serveProjects(ctx context.Context, identity sdd.RequestIdentity return result, nil } -func (s *Server) startProcedure(ctx context.Context, req *mcp.CallToolRequest, args StartProcedureArgs) (*mcp.CallToolResult, ServeResult, error) { +func (s *toolSet) startProcedure(ctx context.Context, req *mcp.CallToolRequest, args StartProcedureArgs) (*mcp.CallToolResult, ServeResult, error) { if strings.TrimSpace(args.Canonical) == "" { return nil, ServeResult{}, toolError("canonical is required") } @@ -608,7 +608,7 @@ func (s *Server) startProcedure(ctx context.Context, req *mcp.CallToolRequest, a return nil, result, err } -func (s *Server) next(ctx context.Context, req *mcp.CallToolRequest, args NextArgs) (*mcp.CallToolResult, ServeResult, error) { +func (s *toolSet) next(ctx context.Context, req *mcp.CallToolRequest, args NextArgs) (*mcp.CallToolResult, ServeResult, error) { ss, err := s.attachedSession(ctx, req, args.Session) if err != nil { return nil, ServeResult{}, err @@ -631,7 +631,7 @@ func (s *Server) next(ctx context.Context, req *mcp.CallToolRequest, args NextAr return nil, result, err } -func (s *Server) abandon(ctx context.Context, req *mcp.CallToolRequest, args AbandonArgs) (*mcp.CallToolResult, AbandonResult, error) { +func (s *toolSet) abandon(ctx context.Context, req *mcp.CallToolRequest, args AbandonArgs) (*mcp.CallToolResult, AbandonResult, error) { if args.Instance == "" { if args.Session == "" { return nil, AbandonResult{}, toolError("pass instance (a move to abandon, with its session) or session alone (tear down a session by handle)") @@ -668,7 +668,7 @@ func (s *Server) abandon(ctx context.Context, req *mcp.CallToolRequest, args Aba // on the shell, with the move now listed among the open threads // (d-tac-dbk). Seeded drafts ride the normal dispatch path; park adds no // side channel. -func (s *Server) park(ctx context.Context, req *mcp.CallToolRequest, args ParkArgs) (*mcp.CallToolResult, ParkResult, error) { +func (s *toolSet) park(ctx context.Context, req *mcp.CallToolRequest, args ParkArgs) (*mcp.CallToolResult, ParkResult, error) { ss, err := s.attachedSession(ctx, req, args.Session) if err != nil { return nil, ParkResult{}, err @@ -700,7 +700,7 @@ func (s *Server) park(ctx context.Context, req *mcp.CallToolRequest, args ParkAr // call (d-tac-dbk). The response names the label and every discarded thread — // nothing vanishes silently. Holding the handle is the authorization // (d-cpt-aen); a session this server holds replayed leaves the cache with it. -func (s *Server) abandonSession(ctx context.Context, req *mcp.CallToolRequest, args AbandonArgs) (*mcp.CallToolResult, AbandonResult, error) { +func (s *toolSet) abandonSession(ctx context.Context, req *mcp.CallToolRequest, args AbandonArgs) (*mcp.CallToolResult, AbandonResult, error) { identity := s.requestIdentity(req) id := sdd.SessionID(strings.TrimSpace(args.Session)) if id == "" { @@ -732,7 +732,7 @@ func (s *Server) abandonSession(ctx context.Context, req *mcp.CallToolRequest, a // caller presents — the reorientation. Presenting the handle declares the // caller needs re-serving, so the ledger-derived served-once memory resets // there and the complete position serves in full (d-cpt-aen). -func (s *Server) resumeSession(ctx context.Context, req *mcp.CallToolRequest, args ResumeSessionArgs) (*mcp.CallToolResult, ResumeSessionResult, error) { +func (s *toolSet) resumeSession(ctx context.Context, req *mcp.CallToolRequest, args ResumeSessionArgs) (*mcp.CallToolResult, ResumeSessionResult, error) { ss, err := s.attachedSession(ctx, req, args.Session) if err != nil { return nil, ResumeSessionResult{}, err @@ -750,7 +750,7 @@ func (s *Server) resumeSession(ctx context.Context, req *mcp.CallToolRequest, ar return nil, mapped, err } -func (s *Server) bindBranch(ctx context.Context, req *mcp.CallToolRequest, args BindBranchArgs) (*mcp.CallToolResult, BindBranchResult, error) { +func (s *toolSet) bindBranch(ctx context.Context, req *mcp.CallToolRequest, args BindBranchArgs) (*mcp.CallToolResult, BindBranchResult, error) { if args.Branch != strings.TrimSpace(args.Branch) { return nil, BindBranchResult{}, toolError("branch must not have leading or trailing whitespace") } @@ -777,7 +777,7 @@ func (s *Server) bindBranch(ctx context.Context, req *mcp.CallToolRequest, args // or rebind, auto-starting the default shell when none is running — a // resumed pre-shell session gains a base to land on (d-tac-bfc). -func (s *Server) stageAttachment(ctx context.Context, req *mcp.CallToolRequest, args StageAttachmentArgs) (*mcp.CallToolResult, StageAttachmentResult, error) { +func (s *toolSet) stageAttachment(ctx context.Context, req *mcp.CallToolRequest, args StageAttachmentArgs) (*mcp.CallToolResult, StageAttachmentResult, error) { if err := validAttachmentName(args.Name); err != nil { return nil, StageAttachmentResult{}, toolError("name: %v", err) } @@ -833,7 +833,7 @@ func validAttachmentName(name string) error { // readScope is what a free read takes from its session: the project it runs // in — the home project with the session's branch binding, or a project the // caller names that the session may work in (d-tac-1z6, d-cpt-yjc). -func (s *Server) readScope(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, project string) (sdd.ProjectID, string, bool, error) { +func (s *toolSet) readScope(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, project string) (sdd.ProjectID, string, bool, error) { return ss.root.ReadScope(ctx, s.requestIdentity(req), sdd.ProjectID(strings.TrimSpace(project))) } @@ -852,11 +852,11 @@ func viewHint(ss *shellSession) string { " for the full filter/rank/macro vocabulary and the quoting rules" } -func (s *Server) logRootRead(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, tool string, full, summary []string) error { +func (s *toolSet) logRootRead(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, tool string, full, summary []string) error { return ss.root.LogRead(ctx, s.requestIdentity(req), tool, full, summary) } -func (s *Server) search(ctx context.Context, req *mcp.CallToolRequest, args SearchArgs) (*mcp.CallToolResult, SearchResult, error) { +func (s *toolSet) search(ctx context.Context, req *mcp.CallToolRequest, args SearchArgs) (*mcp.CallToolResult, SearchResult, error) { if len(args.Terms) == 0 && args.Query == "" { return nil, SearchResult{}, toolError("pass terms (text mode), query (vector mode), or both (hybrid)") } @@ -899,7 +899,7 @@ func (s *Server) search(ctx context.Context, req *mcp.CallToolRequest, args Sear } -func (s *Server) view(ctx context.Context, req *mcp.CallToolRequest, args ViewArgs) (*mcp.CallToolResult, ViewResult, error) { +func (s *toolSet) view(ctx context.Context, req *mcp.CallToolRequest, args ViewArgs) (*mcp.CallToolResult, ViewResult, error) { if strings.TrimSpace(args.Layout) == "" { return nil, ViewResult{}, toolError("layout is required") } @@ -1059,7 +1059,7 @@ func capValue(s string) string { return fmt.Sprintf("%s\n\n[collected value truncated at %d of %d bytes — persisted in full, compressed here]", bounded.Text, bounded.Cut.KeptBytes, bounded.Cut.TotalBytes) } -func (s *Server) show(ctx context.Context, req *mcp.CallToolRequest, args ShowArgs) (*mcp.CallToolResult, ShowResult, error) { +func (s *toolSet) show(ctx context.Context, req *mcp.CallToolRequest, args ShowArgs) (*mcp.CallToolResult, ShowResult, error) { if len(args.IDs) == 0 { return nil, ShowResult{}, toolError("ids is required") } @@ -1093,7 +1093,7 @@ func (s *Server) show(ctx context.Context, req *mcp.CallToolRequest, args ShowAr } -func (s *Server) readAttachment(ctx context.Context, req *mcp.CallToolRequest, args ReadAttachmentArgs) (*mcp.CallToolResult, ReadAttachmentResult, error) { +func (s *toolSet) readAttachment(ctx context.Context, req *mcp.CallToolRequest, args ReadAttachmentArgs) (*mcp.CallToolResult, ReadAttachmentResult, error) { maxBytes := args.MaxBytes if maxBytes == 0 { maxBytes = 65536 @@ -1143,7 +1143,7 @@ func (s *Server) readAttachment(ctx context.Context, req *mcp.CallToolRequest, a } -func (s *Server) info(ctx context.Context, req *mcp.CallToolRequest, args InfoArgs) (*mcp.CallToolResult, InfoResult, error) { +func (s *toolSet) info(ctx context.Context, req *mcp.CallToolRequest, args InfoArgs) (*mcp.CallToolResult, InfoResult, error) { ss, err := s.attachedSession(ctx, req, args.Session) if err != nil { return nil, InfoResult{}, err @@ -1164,7 +1164,7 @@ func (s *Server) info(ctx context.Context, req *mcp.CallToolRequest, args InfoAr } -func (s *Server) registryDocs(ctx context.Context, req *mcp.CallToolRequest, args RegistryArgs) (*mcp.CallToolResult, RegistryResult, error) { +func (s *toolSet) registryDocs(ctx context.Context, req *mcp.CallToolRequest, args RegistryArgs) (*mcp.CallToolResult, RegistryResult, error) { ss, err := s.attachedSession(ctx, req, args.Session) if err != nil { return nil, RegistryResult{}, err @@ -1226,7 +1226,7 @@ func composeFraming(ss *shellSession, blocks []string) string { // and vocabulary dedup live only in toRootServeResult: a base mapping drops // those fields, and recording bytes never delivered would stub them for a // later serve that does deliver. -func (s *Server) serveResultBody(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (ServeResult, error) { +func (s *toolSet) serveResultBody(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (ServeResult, error) { res := ServeResult{ Session: string(serve.Session), Project: string(serve.Project), Branch: serve.Branch, Instance: serve.Instance, Procedure: serve.Procedure, Status: serve.Status, @@ -1289,7 +1289,7 @@ func reportSchemaStub(serve *sdd.WorkflowServe) map[string]any { // toRootServeResult converts an engine serve into the tool response and records // what it served in full into the session ledger. The shell's open-work block // lists this dialogue's own threads only; other dialogues never appear. -func (s *Server) toRootServeResult(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (ServeResult, error) { +func (s *toolSet) toRootServeResult(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (ServeResult, error) { res, err := s.serveResultBody(ctx, req, ss, serve) if err != nil { return ServeResult{}, err @@ -1323,7 +1323,7 @@ func (s *Server) toRootServeResult(ctx context.Context, req *mcp.CallToolRequest // toBaseServeResult maps a serve onto the base_junction landing, which // carries no schema or vocabulary — so neither is recorded as served here. -func (s *Server) toBaseServeResult(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (*BaseServe, error) { +func (s *toolSet) toBaseServeResult(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, serve *sdd.WorkflowServe) (*BaseServe, error) { body, err := s.serveResultBody(ctx, req, ss, serve) if err != nil { return nil, err @@ -1342,7 +1342,7 @@ func rootBaseServe(result ServeResult) *BaseServe { } } -func (s *Server) mapRootResume(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, source sdd.WorkflowResumeResult) (ResumeSessionResult, error) { +func (s *toolSet) mapRootResume(ctx context.Context, req *mcp.CallToolRequest, ss *shellSession, source sdd.WorkflowResumeResult) (ResumeSessionResult, error) { result := ResumeSessionResult{ Session: string(source.Session), Project: string(ss.root.Project()), Participant: source.Participant, Label: source.Label, Branch: source.Branch,