Skip to content

feat(mcp): MCP server rebase, refactor, and seven new tools - #71

Open
Wenzel wants to merge 16 commits into
masterfrom
feat/mcp-server
Open

Wenzel wants to merge 16 commits into
masterfrom
feat/mcp-server

Conversation

@Wenzel

@Wenzel Wenzel commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Three logical units on one branch/PR, since GitHub allows only one open PR per
head branch and this one is still under review. Each unit is reviewable on
its own by its commit range.

Unit 1: rebase onto the unauthenticated master (commits 1-4)

Brings the MCP server branch onto post-Auth0 master as three commits instead
of ten, plus a docs-privacy fix. No new tools; answers "do the existing 10
still work with auth gone?" without new-feature noise.

  1. feat: add paginated search sessions
  2. feat: add MCP server exposing OS intelligence to AI agents
  3. docs: keep the private product name out of the public repository
  4. docs: document diff system semantics and MCP constraints

Release blocker fixed: searchWithSession carried
if (include_updates === true && !context.jwt) throw. context.jwt is now
always undefined, so that condition is always true: replaying it unchanged
would ship a search tool that throws on every call passing
include_updates: true. The gate is deleted rather than adapted.

Two pre-existing defects surfaced and fixed: the root npm test script
was bare jest while jest.config.ts is configured for ESM, so any suite
using top-level await import(...) silently failed to compile while the run
still reported success (tests/git-log/index.test.ts was never actually
running); and a regression guard for the "empty filter drops Blob leaves"
gotcha had been broken since the commit that introduced the fix.

Unit 2: tool manifest refactor (commits 5-7)

Structural work only, ahead of the new tools below.

  1. refactor(mcp): add the uniform pagination envelope
  2. refactor(mcp): extract ref and path resolution into resolve.ts
  3. refactor(mcp): make server.ts a tool manifest
  • New mcp/src/resolve.ts: the multi-hop (ref, path) -> hash chains the
    new tools share. Lives in the MCP layer rather than in new GraphQL
    resolvers: the API is a stable open-source artifact with a second consumer,
    and the hops are localhost-cheap.
  • New mcp/src/pagination.ts: the { items, has_more, next_cursor }
    envelope, including a composite cursor for tools that page two connections
    at once.
  • mcp/src/server.ts becomes a manifest that iterates tool modules and
    applies the content/error envelope once: 545 lines to 46.

The pre-existing 10-tool test suite passes unchanged: same names, same
descriptions, same schemas, same behavior, verified word-for-word against the
pre-refactor file during review.

Unit 3: seven new task-shaped tools, docs (commits 8-14)

  1. feat(mcp): add GraphQL documents for the task-shaped read tools
  2. feat(mcp): add get_struct
  3. feat(mcp): add list_symbols and list_structs
  4. feat(mcp): add list_tree
  5. feat(mcp): add list_registry_key
  6. feat(mcp): add git_log and get_commit_capabilities
  7. test(mcp): pin the two silent-empty-result gotchas, point at the task-shaped tools
  8. docs(mcp): document the 17-tool surface and add a README

Takes the surface from 10 tools to 17. The new tools take (ref, path, name)
and resolve the chain internally; the hash-based tools stay as an escape
hatch, now with a pointer line so an LLM doesn't default to whichever it saw
first.

Tool Signature Hops
list_tree (ref, path, limit?, cursor?) 3
list_registry_key (ref, hive, key_path, limit?, cursor?) 5
get_struct (ref, blob_path, struct_name) 4
list_structs (ref, blob_path, name?, limit?, cursor?) 3
list_symbols (ref, blob_path, name?, limit?, cursor?) 3
git_log (path, entity_type, commit_range, limit?, offset?, ...) 1
get_commit_capabilities (ref) 1

Why task-shaped. With thin tools alone, "show me the _EPROCESS layout in
win11-24h2" costs five round trips and dumps a multi-thousand-row
getBlobsWithSymbols result into context to find one blob hash. get_struct
is the clearest case: reading a struct today means calling diff_nodes with
the same hash on both sides plus status_filter: ["UNCHANGED"]. git_log
closes the largest capability gap: "when did this change, and across which
update" is the question OSWatcher exists to answer.

Pagination. list_tree and list_registry_key page two connections at
once behind one opaque composite cursor; a connection already exhausted is
not requested again.

Two regression tests assert the outgoing request body, not the response:
the filesystem-diff filter and the max_depth: 1 rule, because both bugs
that already bit this branch fail by returning a plausible empty list that an
exception-only test would pass.

Overall verification

Unit tests per tool plus dedicated suites for resolve.ts and pagination.ts.
19 suites / 96 tests pass; build clean; no "grapheos" or em-dashes anywhere
under mcp/.

Live validation against the seeded corpus follows in a separate PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK

@Wenzel Wenzel changed the title feat: MCP server, rebased onto the unauthenticated master feat(mcp): MCP server rebase + tool manifest refactor Sep 9, 2026
@Wenzel Wenzel changed the title feat(mcp): MCP server rebase + tool manifest refactor feat(mcp): MCP server rebase, refactor, and seven new tools Sep 25, 2026
Wenzel and others added 16 commits September 25, 2026 06:39
Records the decisions this branch implements: task-shaped read tools over the
existing thin ones, resolution composed in the MCP layer rather than in new
GraphQL resolvers, one pagination envelope over three mechanisms, and the
deletion of an include_updates gate that guarded against an auth model this
repository no longer has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
search() streams from an async generator over a 6.9M-node graph and cannot be
offset/limited, so paging needs server-side state. Sessions live in a
process-local Map with a 5-minute idle TTL and a configurable cap; the API
cannot be horizontally scaled while sessions are in use.

Also fix the root `test` and `test:watch` scripts to run Jest with
`node --experimental-vm-modules`, matching the `mcp/` subproject: without it,
Jest falls back to a CJS transform and top-level `await import(...)` (used by
this suite and the pre-existing git-log suite) is a syntax error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
A standalone Node ESM subproject under mcp/, speaking Streamable HTTP on port
3001 and calling the GraphQL API with a codegen-generated SDK. Ten tools cover
branches, commits, path traversal, registry roots, symbol-bearing blobs,
paginated search and both diff surfaces.

The API is public, read-only and unauthenticated, so the client forwards no
Authorization header and the SDK is built once at module scope.

Also corrects a stale expectation in tests/diff.test.ts, carried over from the
original history, that predates this rebase: the tool has sent an explicit
filter: ["Tree", "Blob"] since commit 9b6db20 fixed a silent-empty-result
recursion bug, but the test's mock assertions were never updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
The diff system's real gotchas are the empty `filter` silently dropping Blob
leaves and the max_depth=1 rule for leaf-node types. Both are Java procedure
behaviour, not authorization: max_depth carries no auth gate, and there is no
auth model left to gate it with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Every paginated tool returns { items, has_more, next_cursor }, so an LLM learns
one pattern over three mechanisms: connection cursors, gitLog offsets and search
sessions. Tools that page two connections at once encode both endCursors into
one opaque composite cursor; a connection that is already exhausted is simply
not requested on the next page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Task-shaped tools take (ref, path) and resolve the chain internally, so the
multi-hop logic is shared by five or more tools rather than duplicated. It lives
in the MCP layer, not in new GraphQL resolvers: the API is a stable
open-source artifact with a second consumer, and the hops are localhost-cheap.

tools/resolve-ref.ts moves here, it was never a registered tool, only an
internal helper in the wrong directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Each tool module now exports { name, description, schema, handler }, so the long
tool descriptions live next to the code they describe and the content/error
envelope is applied once instead of copy-pasted per handler. server.ts goes from
545 lines to under 100 with no behaviour change; the existing suite is the proof.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Eleven documents backing the seven new tools. The shapes are the ones the
frontend already proves in src/queries.ts, including connection cursor
pagination and edge-name filtering.

By-name lookups get their own document rather than an optional connection
`where`: the filter input is a generated type name that is brittle to
hand-write.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Reading a struct today means calling diff_nodes with the same hash on both sides
plus status_filter ["UNCHANGED"], abusing a diff tool to perform a read. This
takes (ref, blob_path, struct_name) and resolves the chain internally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
By-name lookup is merged into the list tools rather than given separate tools:
an optional exact-match `name` covers both "what does this PE export" and "what
is the address of NtCreateFile" without doubling the surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Browsing a directory took five round trips through traverse_path and raw hashes.
This takes (ref, path). It pages two connections behind one opaque composite
cursor, so the caller sees the same { has_more, next_cursor } envelope as every
other paginated tool, and keeps directories and files as separate lists because
the distinction matters to the caller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Five hops behind one call. hive is an enum mapped internally to
/Windows/System32/config/*: an LLM knows HKLM\SOFTWARE, and making it know the
on-disk hive location is a memorisation tax.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
"When did this entity last change, and across which update" is the question
OSWatcher exists to answer, and the MCP could not ask it. git_log closes that
gap. get_commit_capabilities answers "does this snapshot even have registry or
PDB data", which otherwise surfaces only as a confusing empty result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
…-shaped tools

Both bugs that already bit this branch fail by returning a plausible empty list:
an empty filter silently drops Blob leaf nodes, and a missing max_depth=1 makes
Struct diffs return nothing. A test checking only for the absence of an
exception passes against both, so these assert the outgoing request body.

The three low-level tools now point at their task-shaped replacements; without
that an LLM reaches for whichever tool it encountered first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
master moved to the oswatcher.* procedure namespace (#74) after this branch's
docs were written, which still named it example.diffTreesRecursive. The MCP's
own code never references the procedure by name (it calls diffNodesAt over
GraphQL), so this is a docs-only correction, not a behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant