Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ TypeScript packages for the [Omnigraph](https://github.com/ModernRelay/omnigraph

The SDK targets the `omnigraph-server` version in **`package.json#omnigraph.serverVersion`**. By default, the source is the matching `vX.Y.Z` tag. Before a server release exists, **`omnigraph.serverRef`** may temporarily pin a full immutable commit SHA. The OpenAPI spec, MCP reference documents, and live CI server all use that same source. Branch names and abbreviated SHAs are rejected.

The **v0.10** line targets `omnigraph-server` **v0.10.0**. Upgrade the CLI, server, and client integrations together; see the [v0.9 migration notes](packages/sdk/README.md#migrating-from-v09). Development source pins remain supported, but block publishing through both the release workflow and each package's `prepublishOnly` hook.
The **v0.11** line targets `omnigraph-server` **v0.11.0**. Upgrade the CLI, server, and client integrations together; see the [v0.10 migration notes](packages/sdk/README.md#migrating-from-v010). Development source pins remain supported, but block publishing through both the release workflow and each package's `prepublishOnly` hook.

`scripts/gen-version.ts` stamps the target version as `SERVER_VERSION`. CI checks that the bundled spec matches the pinned source byte for byte and runs live e2e tests against it: a checksum-verified release binary for tags, or a source build for commit pins.

Expand All @@ -37,7 +37,7 @@ In practice: server cuts `0.4.2`, SDK ships `0.4.0` (and `0.4.1`, `0.4.2`, … a

## Workflow when omnigraph cuts a new release

1. Bump `package.json#omnigraph.serverVersion` to the new tag (e.g., `0.10.0`). If upgrading from a source-pinned candidate, remove `omnigraph.serverRef` after the release tag exists.
1. Bump `package.json#omnigraph.serverVersion` to the new tag (e.g., `0.11.0`). If upgrading from a source-pinned candidate, remove `omnigraph.serverRef` after the release tag exists.
2. `pnpm run sync-spec` — fetches the matching `openapi.json` into `spec/`.
3. `pnpm run generate` — regenerates `packages/sdk/src/generated/` and `packages/sdk/src/version.gen.ts`.
4. Commit `spec/openapi.json`, `packages/sdk/src/generated/`, `packages/sdk/src/version.gen.ts`, and the bumped `package.json`. The PR shows the full upstream change.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"packageManager": "pnpm@9.15.0",
"omnigraph": {
"serverVersion": "0.10.0"
"serverVersion": "0.11.0"
},
"scripts": {
"sync-spec": "tsx scripts/sync-spec.ts",
Expand Down
11 changes: 8 additions & 3 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,15 @@ Mutating (`destructiveHint: true` where appropriate — hosts should surface con

There is **no `schema_apply` tool**: a cluster-managed graph rejects HTTP schema apply (409). Schema is read-only here (`schema_get`); evolve it via `omnigraph cluster apply`.

### v0.10 writes, errors, and change pages
### v0.11 writes, errors, and change pages

Successful `mutate` and `load` results include the exact `commit` receipt from
publication. `mutate` returning `commit: null` means a successful no-op. Comparing
publication. A data `mutate` returning `commit: null` means a successful no-op.
A branch statement (`branch create|delete|merge …`, server 0.11) is sent without
a branch — the server refuses one with a target — and reports its effect in
`outcome`: create and delete return `commit: null` although they changed state,
and a merge's `commit` is the target head after the merge, which a concurrent
writer may already have moved. Comparing
branch heads before and after is not a receipt: another writer may advance the
head, and a timed-out operation may still be running.

Expand Down Expand Up @@ -118,7 +123,7 @@ Best-practice references are fetched from the same pinned upstream contract as
the SDK, not moving `main`. Operator/CLI examples are not additional MCP tools;
available types, properties, and vector dimensions come from the live schema.
The older `remote-ops` reference is intentionally excluded until its blanket
retry and branch-head verification advice is refreshed for v0.10. The write/error
retry and branch-head verification advice is refreshed for v0.11. The write/error
rules above and the server's initialization instructions are authoritative for
this MCP version.

Expand Down
7 changes: 6 additions & 1 deletion packages/mcp/cookbook-descriptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"search": {
"title": "Vector and full-text search",
"description": "Read before using nearest/bm25/rrf. Covers scope-first-rank-second queries, schema-declared embeddings, and the trailing-`limit` requirement. Match vector dimensions and models to the live deployment; CLI examples are not MCP tools."
},
"changes": {
"title": "Commits, change feeds and branch statements",
"description": "Read before `commits_changes`, `changes_poll`, or a conditional `mutate`. Covers exact read/write positions (`graphCommitId` → `ifGraphCommit`, 412 means no effects), durable feed consumption, 410 baseline resets, and the v0.11 branch statements accepted by `mutate` (their `outcome` receipts, and why a merge's `commit` is not an exact-attempt receipt)."
}
},
"skipped": {
Expand All @@ -24,6 +28,7 @@
"server-policy": "Server-deployment concerns (Cedar policy, auth tokens), out of scope for an MCP client.",
"cluster": "Operator/CLI concern — declaring and converging a deployment via cluster.yaml + `omnigraph cluster apply`. The MCP wraps the data-plane HTTP API, which cannot apply cluster config.",
"migrations": "Pre-0.7.0 → 0.7.0 migration reference for config files, CLI flags, and command spellings — a CLI/operator guide, not actionable through the HTTP API the MCP wraps.",
"stored-queries": "Stored-query declaration is a cluster/operator concern (cluster.yaml); the MCP wraps the data-plane HTTP API and does not currently expose a stored-query invoke tool."
"stored-queries": "Stored-query declaration is a cluster/operator concern (cluster.yaml); the MCP wraps the data-plane HTTP API and does not currently expose a stored-query invoke tool.",
"blobs": "Blob cells are read through the dedicated `GET`/`HEAD /blob` endpoints, which this MCP does not wrap; writing a Blob (`base64:` or URI) uses the ordinary `mutate`/`load` paths the other references cover."
}
}
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modernrelay/omnigraph-mcp",
"version": "0.10.0",
"version": "0.11.0",
"description": "MCP server exposing an Omnigraph database to LLM clients (Tools + Resources, stdio transport).",
"license": "MIT",
"repository": {
Expand Down
37 changes: 32 additions & 5 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ After schema, consult the matching best-practices resource for the task at hand:
- omnigraph://best-practices/schema — to understand the .pg schema before writing
- omnigraph://best-practices/search — before nearest/bm25/rrf queries

These references also contain operator/CLI examples, not additional MCP tools. The live schema determines available types, properties, and vector dimensions; example models and node names are not deployment guarantees. The write and error rules below are the v0.10 MCP contract.
These references also contain operator/CLI examples, not additional MCP tools. The live schema determines available types, properties, and vector dimensions; example models and node names are not deployment guarantees. The write and error rules below are the v0.11 MCP contract.

Workflow norms (violating these breaks things or silently corrupts data):

1. .gq edges use lowerCamelCase even though the schema declares them PascalCase. No top-level \`mutation { }\` wrapper — every block is \`query name($p: T) { insert|update|delete ... }\`. Dispatch writes via \`mutate\`, not \`query\`.
2. Parameterize. Pass values via \`params\`, never interpolate into the query body. Declare typed params: \`query foo($slug: String) { ... }\`.
3. \`nearest\`, \`bm25\`, and \`rrf\` require a trailing \`limit N\` — they are ordering operators, not filters.
4. \`load mode: "merge"\` upserts stable keys; it is not request deduplication. Reconcile an ambiguous outcome before replaying. \`"overwrite"\` replaces supplied types. \`"append"\` fails on key collision.
5. Successful mutations and loads return an exact \`commit\` receipt. A mutation with \`commit: null\` is a successful no-op, not a failed write. A separate branch-head read cannot prove which writer committed. A timeout or lost response leaves the outcome unknown: verify the intended content and relevant commit history before considering a replay; never infer retry safety from an unchanged head or node type name.
5. Successful mutations and loads return an exact \`commit\` receipt. A data mutation with \`commit: null\` is a successful no-op, not a failed write. A branch statement (\`branch create|delete|merge …\`) reports its effect in \`outcome\`: create and delete return \`commit: null\` although they changed state, and a merge's \`commit\` is the target head after the merge, which a concurrent writer may already have moved. A separate branch-head read cannot prove which writer committed. A timeout or lost response leaves the outcome unknown: verify the intended content and relevant commit history before considering a replay; never infer retry safety from an unchanged head or node type name.
6. For read-modify-write, use \`query.graphCommitId\` as \`mutate.ifGraphCommit\`. It selects the dedicated conditional-write route; HTTP 412 with \`preconditionFailure\` means no effects. Re-read and reconsider the change instead of blindly replaying it. Never fall back to an unconditional mutation when the conditional route is unavailable.
7. Risky/large writes: \`branches_create\` from main → \`load\` onto the branch → verify → \`branches_merge\` → \`branches_delete\`.
8. Schema is read-only over this MCP. \`schema_get\` returns the active .pg source; there is no \`schema_apply\` tool. A cluster-managed graph rejects HTTP schema apply (409) — schema changes go through \`omnigraph cluster apply\` (an operator/CLI action), not an agent tool.
Expand Down Expand Up @@ -113,6 +113,29 @@ const FeedStart = z.union([
z.string().startsWith('after:').min(7).transform((value) => value as `after:${string}`),
]);

/**
* Whether a query text is a v0.11 branch statement (`branch create|delete|merge|list …`, RFC 0055).
* Mirrors the engine grammar: leading whitespace and `//` / `/* *\/` comments, then the keyword
* `branch` on a word boundary. A statement names its branches itself, and the server refuses one
* sent with a request target — so the configured default branch must not be applied to it.
*/
export function isBranchStatement(text: string): boolean {
let i = 0;
for (;;) {
while (i < text.length && ' \t\r\n'.includes(text[i]!)) i++; // the engine's WHITESPACE
if (text.startsWith('//', i)) {
const nl = text.indexOf('\n', i);
if (nl < 0) return false;
i = nl + 1;
} else if (text.startsWith('/*', i)) {
const end = text.indexOf('*/', i + 2);
if (end < 0) return false;
i = end + 2;
} else break;
}
return text.startsWith('branch', i) && !/[A-Za-z0-9_]/.test(text[i + 6] ?? '');
}

export function createOmnigraphMcpServer(opts: CreateServerOptions): McpServer {
const og = new Omnigraph({
baseUrl: opts.baseUrl,
Expand Down Expand Up @@ -188,7 +211,8 @@ export function createOmnigraphMcpServer(opts: CreateServerOptions): McpServer {
query,
name,
params,
branch: snapshot ? branch : (branch ?? defaultBranch),
// A branch statement names its branches itself; never add the default target to it.
branch: snapshot || isBranchStatement(query) ? branch : (branch ?? defaultBranch),
snapshot,
}));
},
Expand Down Expand Up @@ -298,7 +322,10 @@ export function createOmnigraphMcpServer(opts: CreateServerOptions): McpServer {
'Run a .gq mutation (insert/update/delete) against a branch. Canonical write ' +
'endpoint as of server 0.6.0 (successor to `change`). Multi-statement mutations ' +
'are atomic at the commit boundary. Returns affectedNodes / affectedEdges counts and an exact ' +
'commit receipt (null for a successful no-op). ifGraphCommit requires the branch head from a prior ' +
'commit receipt (null for a successful no-op). A branch statement (`branch create|delete|merge …`, ' +
'server 0.11) is sent without a branch and reports its effect in `outcome` instead: create and ' +
'delete return commit null although they changed state, and a merge\'s commit is the target head ' +
'read after the merge, not proof the merge alone published it. ifGraphCommit requires the branch head from a prior ' +
'query and uses the dedicated conditional route; stale heads fail with 412 before effects.',
inputSchema: {
query: z.string().min(1),
Expand All @@ -313,7 +340,7 @@ export function createOmnigraphMcpServer(opts: CreateServerOptions): McpServer {
query,
name,
params,
branch: branch ?? defaultBranch,
branch: isBranchStatement(query) ? branch : (branch ?? defaultBranch),
}, { ifGraphCommit })),
);

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/src/version.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
/**
* The MCP server package version reported in the MCP initialize response.
*/
export const MCP_PACKAGE_VERSION = "0.10.0";
export const MCP_PACKAGE_VERSION = "0.11.0";
41 changes: 40 additions & 1 deletion packages/mcp/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { describe, expect, it } from 'vitest';
import { SERVER_VERSION } from '@modernrelay/omnigraph';
import { createOmnigraphMcpServer, type CreateServerOptions } from '../src/server';
import { createOmnigraphMcpServer, isBranchStatement, type CreateServerOptions } from '../src/server';
import { MCP_PACKAGE_VERSION } from '../src/version.gen';

const COMMIT = {
Expand Down Expand Up @@ -364,6 +364,7 @@ describe('omnigraph-mcp server', () => {
const uris = r.resources.map((res) => res.uri).sort();
expect(uris).toEqual(
[
'omnigraph://best-practices/changes',
'omnigraph://best-practices/data',
'omnigraph://best-practices/index',
'omnigraph://best-practices/queries',
Expand Down Expand Up @@ -489,3 +490,41 @@ it('branches_create honours configured defaultBranch when `from` is omitted', as
expect(r.isError).toBe(true);
});
});

describe('branch statements (server 0.11)', () => {
it.each([
['branch create "x" from "main"', true],
[' // note\nbranch merge "x" into "main"', true],
['/* c */ branch delete "x"', true],
['branch list', true],
['query branches() { match { $p: Person } return { $p.name } }', false],
['branches', false],
['branch_x', false],
['/* unterminated branch create "x"', false],
['\u00a0branch list', false], // not engine whitespace: the server would not parse it as a statement
['\t\r\nbranch list', true],
])('isBranchStatement(%j) is %s', (text, expected) => {
expect(isBranchStatement(text)).toBe(expected);
});

it.each([
['mutate', '/mutate', 'branch create "x" from "main"'],
['query', '/query', 'branch list'],
])('%s sends a statement without the default branch, and a data op with it', async (tool, route, statement) => {
const bodies: Array<Record<string, unknown>> = [];
const { client } = await setup({ defaultBranch: 'review', fetch: async (input, init) => {
bodies.push(JSON.parse(String(init?.body)));
expect(flatPath(String(input))).toBe(route);
const body = tool === 'mutate'
? { branch: 'x', affected_nodes: 0, affected_edges: 0, commit: null, outcome: { kind: 'created', name: 'x', from: 'main' } }
: { columns: [], rows: [] };
return new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' } });
} });
const result = await client.callTool({ name: tool, arguments: { query: statement } });
expect(result.isError).toBeFalsy();
expect(bodies[0]).not.toHaveProperty('branch');
if (tool === 'mutate') expect(toolJson(result).outcome).toEqual({ kind: 'created', name: 'x', from: 'main' });
await client.callTool({ name: tool, arguments: { query: 'query q() { match { $p: Person } return { $p.name } }' } });
expect(bodies[1]?.branch).toBe('review');
});
});
28 changes: 24 additions & 4 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ npm install @modernrelay/omnigraph

Requires **Node 22+** (uses native `fetch` and web streams). Browser support depends on server CORS; browsers also hide manual cross-origin redirects, so inspecting external Blob descriptors requires a server-side runtime.

**v0.10 targets omnigraph-server v0.10.0.** Upgrade the CLI, server, and client
integrations together; see [Migrating from v0.9](#migrating-from-v09).
**v0.11 targets omnigraph-server v0.11.0.** Upgrade the CLI, server, and client
integrations together; see [Migrating from v0.10](#migrating-from-v010).

## First call

Expand All @@ -41,7 +41,7 @@ That's the whole pattern: instantiate once (with a `graphId`), call methods, get

> **`graphId` is required (server 0.7.0).** `omnigraph-server` is cluster-only: every graph-scoped operation is served under `/graphs/{graphId}/…`. A graph-scoped call without a `graphId` throws `ConfigurationError` before hitting the network. Only `og.health()` and `og.graphs.list()` work without one — use the latter to discover ids, then [`og.graph(id)`](#multi-graph-clusters). This SDK targets the matching server release (see [Server compatibility](#server-compatibility)); for a 0.6.x (flat-route) server, stay on `@modernrelay/omnigraph@0.6.x`.

Use **`og.query()`** (read), **`og.mutate()`** (write), and **`og.load()`** (bulk load). Deprecated aliases were removed from the SDK in v0.7; v0.10 does not promise compatibility with older server minor versions.
Use **`og.query()`** (read), **`og.mutate()`** (write), and **`og.load()`** (bulk load). Deprecated aliases were removed from the SDK in v0.7; v0.11 does not promise compatibility with older server minor versions.

## What you can do

Expand All @@ -68,10 +68,15 @@ const { affectedNodes, affectedEdges } = await og.mutate({
});
```

Multi-statement mutations publish atomically. Successful mutations return an
Multi-statement mutations publish atomically. Successful data mutations return an
exact `commit` receipt; `commit: null` means a successful no-op. Load methods
also return their exact commit, plus `nodes`, `edges`, and `totalEntities`.

A branch statement (`og.mutate({ query: 'branch create "x" from "main"' })`, server 0.11)
is sent without `branch` and reports its effect in `outcome`. Its `commit` is not a
receipt: create and delete return `null` although they changed state, and a merge's
`commit` is the target head after the merge, which a concurrent writer may already have moved.

### Conditional mutations

```ts
Expand Down Expand Up @@ -290,6 +295,21 @@ Published SDKs track server **major.minor**, with independent patch versions.
and runs live e2e against the same release, or the exact immutable source pin
while an upcoming release is being prepared.

### Migrating from v0.10

Additive on the wire; no public name changed. What to know:

- **Entity identity** on graphs created by v0.11 (format 9) is a top-level `id` in load, export and
baseline envelopes — `ExportRecord` now carries it. `data.id` is an ordinary user property there;
legacy graphs still accept it as identity when top-level `id` is absent. Before rebuilding an older
export into a new graph, move `data.id` to the top level
([upgrade procedure](https://github.com/ModernRelay/omnigraph/blob/v0.11.0/docs/user/operations/upgrade.md#rebuild)).
In queries, identity is `@id` (edges: `@src`, `@dst`).
- **Branch statements** (`branch create|delete|merge …`) run through `og.mutate()` without `branch`;
the effect is `Change.outcome` (`BranchOutcome`). See the `mutate` doc comment for receipt semantics.
- **New:** `og.readiness()` (`GET /readyz`, a 503 is an answer, not an error), `GraphList.quarantined`,
`Schema.systemColumns` (`SystemColumns`).

### Migrating from v0.9

This is a coordinated upgrade, not a rolling one: stop application traffic
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modernrelay/omnigraph",
"version": "0.10.0",
"version": "0.11.0",
"description": "TypeScript SDK for the Omnigraph HTTP API.",
"license": "MIT",
"repository": {
Expand Down
Loading
Loading