Skip to content
Merged
18 changes: 15 additions & 3 deletions docs/book/src/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ integration, from its own MCP server: `quipu mcp --db <path>` over stdio, or
`quipu-server` at `/mcp` over HTTP. (Bobbin embeds Quipu but serves its own
`knowledge_*` tools, not these.)

The registry (`tool_definitions()`) exposes **79 tools** in a default build, or
**81** when built with the `owl` feature (which adds `quipu_load_ontology` and `quipu_explain`).
The registry (`tool_definitions()`) exposes **81 tools** in a default build, or
**83** when built with the `owl` feature (which adds `quipu_load_ontology` and `quipu_explain`).
(The counts are pinned by tests in `src/mcp/tests.rs`, which also check this
page and the README against the manifest.)

Expand Down Expand Up @@ -1075,16 +1075,28 @@ Fields: `update` (required), `actor`, `source`.

### `quipu_events`

Read a bounded event batch without committing a consumer cursor. since overrides consumer. types is a comma-separated event-type filter; group is an episode group, not a graph selector.
Read a bounded batch of ROOT's events without committing a consumer cursor. since overrides consumer. types is a comma-separated event-type filter; group is an episode group, not a graph selector. A named graph's events are quipu_graph_events.

Fields: `since`, `limit`, `types`, `group`, `consumer`.

### `quipu_graph_events`

Read a bounded batch of ONE registered committed named graph's events (its own log and offsets) without committing a cursor. Same filters as quipu_events.

Fields: `graph` (required), `since`, `limit`, `types`, `group`, `consumer`.

### `quipu_events_commit`

Durably set a consumer cursor. Lower offsets explicitly rewind; delivery alone never commits.

Fields: `consumer_id` (required), `offset` (required).

### `quipu_graph_events_commit`

Durably set a consumer cursor in one registered committed named graph's event log. Lower offsets explicitly rewind.

Fields: `consumer_id` (required), `graph` (required), `offset` (required).

### `quipu_changes`

Read whole transaction change pages. graph scopes an exact graph; use next_tx for the next page. No cursor write.
Expand Down
41 changes: 37 additions & 4 deletions docs/book/src/reference/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,26 @@ Returns `{events, next_offset, lag, committed_offset?}`; pass `next_offset`
back as `since` (or commit it) to page forward — polling is a fixpoint, not a
rewind.

`/events` serves ROOT's events, as it always has, and refuses a `graph`
parameter (HTTP 400).

### `GET /events/graph`

`graph=<iri>` (required) plus the `/events` parameters: one registered
committed named graph's events. A commit to such a graph emits
`episode.ingested` and `entity.added` / `entity.updated` (one per entity per
transaction, "added" judged within that graph), each carrying a `graph` key.
Overlays and unregistered graphs emit nothing, and an unregistered or overlay
`graph` returns HTTP 400. The graph's events are a separate log with its own
offsets: `since`, `next_offset`, `consumer` resume and `lag` are all in that
log, and ROOT's offsets and cursors are untouched by board writes.

Named graphs have their own routes, not a `graph` parameter on `/events`,
so that an older server cannot misread them. A server that predates
named-graph events ignores an unknown query parameter and would serve ROOT's
events; it answers an unknown route 404. So against an older server a graph
consumer gets a 404, never ROOT's events and never a ROOT cursor write.

### `GET /events/stream` and `GET /changes/stream`

Read-only Server-Sent Events replay of the corresponding durable feed. Supply a
Expand All @@ -2233,8 +2253,11 @@ frames have event type `quipu.events`, a delivered event-offset ID, and the
ordinary `/events` page as JSON data. Change frames use `quipu.changes`, a
transaction-ID cursor and the ordinary `/changes` page as JSON data.

Event streams accept `types` and `group`; change streams accept `graph`,
including a registered named graph. Unrelated filter kinds and invalid resume
Event streams accept `types`, `group` and `graph` (one registered committed
named graph, read from its own log as on `/events/graph`; a server that
predates named-graph events refuses `graph` on an event stream with HTTP
400); change streams accept `graph`, including a
registered named graph. Unrelated filter kinds and invalid resume
cursors return HTTP 400. Exhausted stream capacity returns HTTP 503.

Delivery does not acknowledge a consumer. Commit applied data and its cursor
Expand All @@ -2252,8 +2275,18 @@ before the output check; this is not a total heap bound. See the

### `POST /events/commit`

`{"consumer_id", "offset"}` — durably record a consumer's cursor. Any offset
≥ 0 is accepted, including a **lower** one: that is the explicit replay knob.
`{"consumer_id", "offset"}` — durably record a ROOT consumer's cursor. Any
offset ≥ 0 is accepted, including a **lower** one: that is the explicit replay
knob. A `graph` field is refused (HTTP 400): a server that predates
named-graph events would ignore it and upsert this consumer's ROOT cursor
before answering.

### `POST /events/graph/commit`

`{"consumer_id", "graph", "offset"}` — durably record a consumer's cursor in
one registered committed named graph's event log (the response echoes
`graph`). The same `consumer_id` may also hold a ROOT cursor; the two never
touch. An older server answers this route 404 and writes nothing.

### Refusal events (`write.refused`)

Expand Down
8 changes: 4 additions & 4 deletions docs/book/src/why-quipu.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Quipu's thesis: **start strict, use agents to bear the cost of strictness.**
- **Graph projection** — materialize subgraphs into petgraph for centrality, connected components, shortest path algorithms.
- **Federation** — a `GraphProvider` trait for multi-source queries, with a `RemoteProvider` (behind the `remote` feature) built from `federation.remotes` config. The server health-checks every configured remote at startup, and `POST /query` with `"federated": true` fans out through the federated provider, reporting which members answered. Remotes carry declared trust labels at the federation edge, so a federated answer composes the labels of every member that contributed rather than silently inheriting the caller's. Federation config is read-side only: adding a remote never turns it into an outbound replication target or bypasses the share scrub/import boundary.
- **Graph explorer** — the web UI draws the whole node-link view from a single `POST /graph` payload (nodes plus index-addressed edges), laid out with a Barnes-Hut force simulation on canvas. No CDN, so it renders on an air-gapped deploy.
- **Four interfaces** — Rust crate (embed), CLI (`quipu`), REST API (`quipu-server`), and built-in web UI with embeddable web components. Plus 79 MCP tools for agent integration (81 with the `owl` feature).
- **Four interfaces** — Rust crate (embed), CLI (`quipu`), REST API (`quipu-server`), and built-in web UI with embeddable web components. Plus 81 MCP tools for agent integration (83 with the `owl` feature).
- **"SQLite energy"** — single process, no server required, inspect with `sqlite3`, back up with `cp`.
- **Automated releases** — release-plz bumps versions from conventional commits, generates changelogs via git-cliff, and creates GitHub releases. Version discovery is `git_only = true`: the baseline comes from this repository's tags, never the unrelated `quipu` crate on crates.io. CI runs fmt, clippy, tests, and markdown lint on every push. `/version` also reports the deployed git SHA, which matters because a deployment can legitimately sit AHEAD of the newest tag — the SHA, not the version string, identifies what is actually running.

Expand Down Expand Up @@ -305,7 +305,7 @@ The reasoner adds forward-chaining inference over the EAVT fact log:
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌──────┴──────┐
│ MCP Tools │ │ REST API │ │ Rust API │
│ (79 tools) │ │ + Web UI │ │ (crate) │
│ (81 tools) │ │ + Web UI │ │ (crate) │
└─────┬─────┘ └─────┬─────┘ └──────┬──────┘
└────────────────┼────────────────┘
│
Expand Down Expand Up @@ -334,7 +334,7 @@ Quipu is designed as a [Bobbin](https://github.com/scbrown/bobbin) subsystem.
Bobbin holds the thread (code context); Quipu ties knots of structured meaning into it.

Through Quipu's own MCP server (`quipu mcp`, or `quipu-server` at `/mcp`),
agents get 79 MCP tools (81 with the `owl` feature). The two most
agents get 81 MCP tools (83 with the `owl` feature). The two most
commonly used for knowledge-aware context:

**`quipu_context`** — unified knowledge discovery. Bobbin merges the result
Expand Down Expand Up @@ -457,7 +457,7 @@ primitive only, not reachable from the shipped binaries · 🔜 planned.
| Graph explorer | ✅ | Canvas + Barnes-Hut layout, one `POST /graph` payload, no CDN |
| Web components | ✅ | Embeddable `<quipu-*>` elements |
| Semantic Web APIs | ✅ | Spotlight, TPF, OpenRefine reconciliation |
| MCP tools (79; 81 with `owl`) | ✅ | Agent integration |
| MCP tools (81; 83 with `owl`) | ✅ | Agent integration |
| Python bindings | ✅ | `quipu-client` under `python/` — REST client, stdlib-only |
| **Infrastructure** | | |
| Graph projection (petgraph) | ✅ | Centrality, shortest path, etc. |
Expand Down
2 changes: 1 addition & 1 deletion docs/knowledge/repository-knowledge.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
"source": "docs/book/src/reference/rest-api.md",
"subject": "http://aegis.gastown.local/ontology/doc/quipu/docs%2Fbook%2Fsrc%2Freference%2Frest-api.md#rest-api",
"summary": "The native server exposes graph operations over HTTP; authentication, request budgets and provenance headers have separate roles.",
"source_sha256": "44abde06edafbfaa99807e36a5bee20f69912c2525403d4f017a088cfe9c1942",
"source_sha256": "3a1d7d312e0ea77ac965b072abe7bd673f71898b1b39d7e4e608ceaecccf63b9",
"properties": {
"interfaceKind": "REST",
"publicName": "quipu-server HTTP"
Expand Down
6 changes: 4 additions & 2 deletions src/http_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub const WRITE_ENDPOINTS: &[&str] = &[
"/graph/freeze", // deep freeze: relocates rows, mutates registry + attachments
"/graph/thaw", // restores rows, mutates registry + attachments
"/events/commit", // durable consumer cursor upsert (event-log P1)
"/events/graph/commit", // a named graph's consumer cursor upsert (event-push §4)
"/subscriptions", // push-subscription registry create/list/delete (event-log P2)
"/datasets", // named-dataset registry create/remove (quipu #69) + meta-graph mirror
"/update", // SPARQL 1.1 Update mutates default and named graphs
Expand Down Expand Up @@ -213,10 +214,11 @@ pub const READ_ENDPOINTS: &[&str] = &[
"/entity/{iri}/html",
"/entity_history",
"/transactions",
"/events", // pull-batch event log read (event-log P1); the commit half is a write
"/events", // pull-batch event log read (event-log P1); the commit half is a write
"/events/graph", // one named graph's event log read (event-push §4)
"/events/stream", // read-only replay; delivery never commits a consumer ACK
"/changes/stream", // read-only transaction replay, including named graphs
"/changes", // fact-level change feed (quipu-2ae): pull-only, cursor is a tx id
"/changes", // fact-level change feed (quipu-2ae): pull-only, cursor is a tx id
"/spotlight",
"/fragments",
"/reconcile",
Expand Down
58 changes: 57 additions & 1 deletion src/mcp/definitions/parity/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub(super) fn defs() -> Vec<Value> {
}),
serde_json::json!({
"name": "quipu_events",
"description": "Read a bounded event batch without committing a consumer cursor. since overrides consumer. types is a comma-separated event-type filter; group is an episode group, not a graph selector.",
"description": "Read a bounded batch of ROOT's events without committing a consumer cursor. since overrides consumer. types is a comma-separated event-type filter; group is an episode group, not a graph selector. A named graph's events are quipu_graph_events.",
"inputSchema": {
"type": "object",
"properties": {
Expand All @@ -74,6 +74,37 @@ pub(super) fn defs() -> Vec<Value> {
"additionalProperties": false
}
}),
serde_json::json!({
"name": "quipu_graph_events",
"description": "Read a bounded batch of ONE registered committed named graph's events (its own log and offsets) without committing a cursor. Same filters as quipu_events.",
"inputSchema": {
"type": "object",
"properties": {
"graph": {
"type": "string"
},
"since": {
"type": "integer"
},
"limit": {
"type": "integer"
},
"types": {
"type": "string"
},
"group": {
"type": "string"
},
"consumer": {
"type": "string"
}
},
"required": [
"graph"
],
"additionalProperties": false
}
}),
serde_json::json!({
"name": "quipu_events_commit",
"description": "Durably set a consumer cursor. Lower offsets explicitly rewind; delivery alone never commits.",
Expand All @@ -95,6 +126,31 @@ pub(super) fn defs() -> Vec<Value> {
"additionalProperties": false
}
}),
serde_json::json!({
"name": "quipu_graph_events_commit",
"description": "Durably set a consumer cursor in one registered committed named graph's event log. Lower offsets explicitly rewind.",
"inputSchema": {
"type": "object",
"properties": {
"consumer_id": {
"type": "string"
},
"graph": {
"type": "string"
},
"offset": {
"type": "integer",
"minimum": 0
}
},
"required": [
"consumer_id",
"graph",
"offset"
],
"additionalProperties": false
}
}),
serde_json::json!({
"name": "quipu_changes",
"description": "Read whole transaction change pages. graph scopes an exact graph; use next_tx for the next page. No cursor write.",
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1955,13 +1955,13 @@ fn test_tool_definitions() {
assert!(names.contains(&"quipu_merge_apply"));
#[cfg(feature = "owl")]
{
assert_eq!(defs.len(), 81);
assert_eq!(defs.len(), 83);
assert!(names.contains(&"quipu_load_ontology"));
assert!(names.contains(&"quipu_explain"));
}
#[cfg(not(feature = "owl"))]
{
assert_eq!(defs.len(), 79);
assert_eq!(defs.len(), 81);
assert!(!names.contains(&"quipu_load_ontology"));
assert!(!names.contains(&"quipu_explain"));
}
Expand Down
2 changes: 2 additions & 0 deletions src/mcp_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn endpoint(name: &str) -> (Method, String) {
"quipu_health" => return (Method::GET, "/health".into()),
"quipu_version" => return (Method::GET, "/version".into()),
"quipu_events" => return (Method::GET, "/events".into()),
"quipu_graph_events" => return (Method::GET, "/events/graph".into()),
"quipu_changes" => return (Method::GET, "/changes".into()),
"quipu_transactions" => return (Method::GET, "/transactions".into()),
"quipu_entity" => return (Method::GET, "/entity".into()),
Expand All @@ -47,6 +48,7 @@ fn endpoint(name: &str) -> (Method, String) {
"quipu_import_promote" => "/import/promote",
"quipu_acquire_composed" => "/acquire/composed",
"quipu_events_commit" => "/events/commit",
"quipu_graph_events_commit" => "/events/graph/commit",
"quipu_knot_stage" => "/knot/stage",
"quipu_knot_promote" => "/knot/promote",
"quipu_graph_create" => "/graph/create",
Expand Down
2 changes: 2 additions & 0 deletions src/mcp_transport/parity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ fn write_annotations_cover_new_workflows_and_reads_stay_read_only() {
("quipu_reason", true),
("quipu_events", false),
("quipu_events_commit", true),
("quipu_graph_events", false),
("quipu_graph_events_commit", true),
("quipu_changes", false),
("quipu_subscriptions", true),
("quipu_embed_backfill", true),
Expand Down
2 changes: 2 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ async fn main() {
.route("/events/stream", get(feed_stream::events_stream))
.route("/changes/stream", get(feed_stream::changes_stream))
.route("/events/commit", post(events_commit))
.route("/events/graph", get(feed::events_graph_get))
.route("/events/graph/commit", post(feed::events_graph_commit))
// Semantic web APIs (Phase 4)
.route("/spotlight", post(spotlight_handler))
.route("/fragments", get(fragments_handler))
Expand Down
Loading
Loading