Skip to content

fix(plugins): honour schemaId in xnet_query and xnet_create - #637

Merged
crs48 merged 1 commit into
mainfrom
fix/mcp-query-schema-filter
Jul 28, 2026
Merged

fix(plugins): honour schemaId in xnet_query and xnet_create#637
crs48 merged 1 commit into
mainfrom
fix/mcp-query-schema-filter

Conversation

@crs48

@crs48 crs48 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

The bug

xnet_query ignored a schemaId filter and returned nodes of every schema.

Verified against the live desktop app's in-process MCP server: calling
xnet_query with {"schemaId":"xnet://xnet.fyi/Page@1.0.0","limit":3} came
back with a Canvas among the results. The NodeStore was not at fault —
store.list({ schemaId: 'xnet://xnet.fyi/Page@1.0.0' }) returns only Pages.

Root cause

The tool was declared with schema and the handler read toolArgs.schema.
An MCP client that passes schemaId — the field name every node the tools
hand back actually carries — therefore left schema undefined, and
store.list treats an absent schemaId as "every schema". The filter was not
mis-applied; it was dropped, and the widened result was indistinguishable from
a correct one.

xnet_create had the same mismatch, where dropping the argument mints a node
with schemaId: undefined.

The fix

  • Both tools read schemaId first and keep schema as a deprecated alias.
  • A call supplying neither now throws instead of quietly returning everything —
    a missing filter must fail, not widen.
  • Input schemas declare schemaId (required) alongside the alias, and the
    example IRI is a real registered one (xnet://xnet.fyi/Task@1.0.0).

Tests

Three cases added to mcp-server.test.ts, all failing before the fix:

  • a schema-filtered query returns only nodes of that schema (Task seeded
    alongside a Project);
  • schemaId is accepted as an alias and still filters — this one returned both
    nodes before the fix;
  • a query with no schema errors instead of returning every node.

pnpm exec vitest run --project integration packages/plugins → 788 passed.
turbo run typecheck --filter=@xnetjs/plugins and eslint clean.

Full pnpm test: 11258 passed, 3 files failed to load from this worktree's
incomplete node_modules (smol-toml unlinked for packages/cli, electron
binary not installed) — pre-existing and unrelated; CI is the real gate.

🤖 Generated with Claude Code

Both MCP tools read only `schema`, so a client passing `schemaId` — the field
name every node carries — had its filter silently dropped. `store.list` treats
an absent `schemaId` as "every schema", so `xnet_query` answered "my pages"
with nodes of any type; `xnet_create` could mint a node with no schema at all.

Read `schemaId` first, keep `schema` as a deprecated alias, and fail loudly
when neither is supplied rather than widening to every node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-637 July 28, 2026 02:36 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #637.

github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@crs48
crs48 merged commit 942e6c4 into main Jul 28, 2026
19 checks passed
@crs48
crs48 deleted the fix/mcp-query-schema-filter branch July 28, 2026 02:45
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
crs48 added a commit that referenced this pull request Jul 28, 2026
…638)

## What was broken

The desktop app already ran an agent bridge that drives your own
`claude` CLI as the chat model. But the agent had **no path to your
data**.

The MCP wiring sat behind `XNET_BRIDGE_MCP` + `XNET_BRIDGE_MCP_CLI`, and
the CLI entry pointed at `xnet mcp serve` — except `@xnetjs/cli` is not
an Electron dependency, so that path could never resolve in a packaged
app. Even had it resolved, it passed no token, so the local API would
have returned 401. Chat could talk about your workspace but not read or
write it.

## What this does

Hosts the MCP server **in the Electron main process**, over the renderer
store proxy, and points the agent at it over Streamable HTTP on an
ephemeral loopback port. No CLI to spawn, no local-API hop, no
per-session token to plumb across processes. Withhold the tools with
`XNET_BRIDGE_MCP=0`.

Also replaces the local API's hardcoded four-schema stub
(`Schema`/`Task`/`Project`/`Note`) with the renderer's real
`schemaRegistry`, served over the same IPC channel that already carries
store operations. Anything reading that stub was getting a fiction.

### A subtlety worth reviewing

`getAllIRIs()` is synchronous in `SchemaRegistryAPI`, but the registry
lives an IPC hop away in the renderer — so the proxy caches it. Both
subscribers start **before `createWindow()`**, so priming must be
background-with-retry and must never be awaited at boot; awaiting it
deadlocks startup (hit and backed out during development). While
unprimed it **throws** rather than returning `[]`, so "renderer not
ready" cannot read as "this workspace has no schemas".

## Verification

Verified in the live desktop app over CDP, not just unit-tested.

Bridge status from inside the renderer:

```
{ running: true, agent: "claude", url: "http://127.0.0.1:31416", workspaceTools: true }
```

The spawned process, wired as designed:

```
claude -p "..." --output-format stream-json --include-partial-messages --verbose
  --mcp-config "~/Library/Application Support/xnet-desktop/agent-bridge-mcp.json"
  --allowedTools mcp__xnet__*
```

A real chat turn through the bridge returned *"Schema count: 194 · New
page ID: `M6_jJ2XVxc5aKIx45SX3V`"*, and the page was really in the
store:

```
{ id: "M6_jJ2XVxc5aKIx45SX3V", schemaId: "xnet://xnet.fyi/Page@1.0.0",
  title: "Agent Smoke Test 0406" }
pageCount: 29 -> 30
```

**194 schemas is the proof the registry fix works** — the old stub would
have reported 4. The test page was deleted afterwards.

Also confirmed empirically: `createMcpHttpServer` answers POST with
plain `application/json` (no SSE, no `mcp-session-id`), and Claude Code
2.1.220 accepts it as a `{"type":"http"}` MCP server. That was the
load-bearing unknown in this design, so it was probed against the real
CLI before building on it.

Checks: `turbo typecheck` clean, 146 electron + 166 devkit tests green
(6 new covering the priming logic), lint 0 errors.

## Notes

- The `xnet_query` schemaId bug found while verifying this landed
separately in #637.
- Changeset included (`@xnetjs/devkit` minor — new `mcpHttpConfigFor`
export) plus a changelog fragment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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