MCP Reference (tools + resources)
MCP Reference (tools + resources)
What it is. The wip_mcp package (installed as wip-mcp, console script wip-mcp = wip_mcp.server:main) is WIP's Model-Context-Protocol server. It exposes the platform's five stores — registry, def-store, template-store, document-store, reporting-sync — as MCP tools (actions) and resources (wip://… readables), so an AI coding agent can discover, create, and query WIP entities without constructing raw HTTP calls (components/mcp-server/src/wip_mcp/server.py:1-10). It owns no data itself: server.py is a mcp.server.fastmcp.FastMCP tool/resource layer over client.py's WipClient, a thin httpx-based HTTP client to the five stores.
Reaching it. Three transports, selected by CLI flag to wip-mcp / python -m wip_mcp.server:
stdio(default) — for local host-spawned integration (Claude Code, Cursor).--http— streamable HTTP, for cluster/remote deployment (components/mcp-server/src/wip_mcp/server.py:4068-4152).--sse— legacy Server-Sent Events, deprecated in the MCP spec itself; kept for older clients.
For --http/--sse, the server binds MCP_HOST (default 0.0.0.0) / MCP_PORT (default FastMCP's 8000), registers an unauthenticated GET /health liveness probe (for compose/k8s orchestration, checked before the auth middleware so it can be exempted), and — only if API_KEY or WIP_AUTH_LEGACY_API_KEY is set — installs ApiKeyMiddleware, which requires X-API-Key (header) or api_key (query param) on every other path. If neither env var is set, the server starts anyway with only a stderr warning and serves unauthenticated — see gotchas. MCP_ALLOWED_HOST additionally appends to the transport's allowed_hosts/allowed_origins for DNS-rebinding protection.
Underneath, every tool call is itself an authenticated HTTP call from the MCP server to one of the five WIP services: registry (:8001), def-store (:8002), template-store (:8003), document-store (:8004), reporting-sync (:8005) — direct defaults, each overridable by its own *_URL env var, or all five collapsed onto one base via WIP_API_URL (e.g. a router/Caddy front door). Every outbound request carries X-API-Key: <resolved key>, resolved in priority order WIP_API_KEY env → MASTER_API_KEY env → API_KEY env → WIP_API_KEY_FILE (file contents, for rotation without editing client config) → a hardcoded dev default (components/mcp-server/src/wip_mcp/client.py:47-82). See also: Auth & Access; Routing & Ingress.
Conventions that apply.
- Bulk-first 200 OK — the
*_bulktools return the full{total, succeeded, failed, results}envelope; callers must checkresults[i].status. Single-item tools unwrap that same envelope for you and surface a per-item error as a formatted exception instead (see "How tool responses/errors work" below). - Namespace scoping — every write tool resolves its namespace via
WIP_MCP_DEFAULT_NAMESPACEwhen the caller omits one (raises if neither is set); most read/query tools instead treat an omitted namespace as "search everywhere." - Identity-hash / PATCH semantics —
update_documentimplements the platform's RFC 7396 JSON Merge Patch contract end-to-end, including all its documented error codes. - Template-cache TTL (5 s) —
create_template/update_templatecallers are warned in-line to pass an explicittemplate_versionrather than relying on "latest" resolution immediately after a change.
See also: API Conventions; PoNIFs.
Resources
Five static/dynamic Markdown resources registered under wip://… URIs. An MCP client reads these as context, not as callable actions.
wip://conventions
Static text (server.py:70-324). The canonical write-up of: Bulk-First 200 OK; update_document PATCH semantics and its full error-code list; the Idempotent Bootstrap endpoints (PUT /api/registry/namespaces/{prefix} upsert + its deletion_mode safety guards; POST /templates?on_conflict=validate's three-tier compatibility check; the equivalent on_conflict=validate for terminologies/terms); querying tools (query_by_template, run_report_query, get_table_view, export_table_csv); Soft Delete (retain vs full deletion_mode, and the independent exceptions — binary files and mutable-terminology terms are always hard-deletable); Identity & Versioning (template multi-version coexistence, document identity-field dedup rules); Namespaces & Authorization (permission model, open/strict isolation, relationship-document cross-namespace rejection, single- vs multi-namespace API-key namespace derivation); Ontology Relations; Reporting & Aggregation; the Template Cache and Namespace-Config Cache TTLs (5 s each); Pagination (page_size default 50 / max 100, pages field). This is the same source text the API Conventions library doc is generated from — see that doc for the full canonical write-up; this reference only names the sections.
wip://data-model
Static text (server.py:327-464). Covers Terminologies, Terms, Templates (inheritance, versioning, usage: entity|reference|relationship, versioned: true|false, header_fields), Field Types, Semantic Types, Reference Field Types (document/term/terminology/template), File Field config, Array Field config, other field properties (mandatory not required, full_text_indexed), cross-field Validation Rules (CONDITIONAL_REQUIRED, CONDITIONAL_VALUE, MUTUAL_EXCLUSION, DEPENDENCY), Template Draft Mode, Reporting Configuration, Documents, Files, Registry & Synonyms, and Ontology Relations. Same source text the Data Model Reference library doc is generated from.
wip://development-guide
Static text (server.py:467-560). "Building Applications on WIP" — the 4-phase workflow: Phase 1 Exploratory (inventory existing templates/terminologies with get_wip_status, list_namespaces, list_terminologies, list_templates, query_by_template, get_template_fields — reuse, don't recreate); Phase 2 Data Model Design (map the domain to terminologies/templates, plan ontology relations for hierarchical vocabularies, choose identity_fields carefully, apply semantic_types, use the correct field-naming conventions); Phase 3 Implementation (create terminologies → terms → templates via draft mode + activate_template for circular deps → test documents, pinning template_version explicitly); Phase 4 Application Layer (build the frontend with @wip/client/@wip/react; MCP tools remain useful for debugging/queries). Points to the /explore, /design-model, /implement, /build-app slash commands for step-by-step detail. No dedicated Library doc covers this resource, so it is described here in full.
wip://ponifs
Static text (server.py:563-761). WIP's eight PoNIFs (Powerful, Non-Intuitive Features — the reading list's canonical expansion, sourced from this same file):
- Nothing Ever Dies — soft delete is the default; only a namespace explicitly flipped to
deletion_mode: fullallowshard_delete=true. - Template Versioning — update creates a new version, old stays active; existing docs are pinned via
template_id(canonical across versions). - Document Identity — the identity-field hash decides create vs. version; zero identity fields = append-only, no PATCH path (
error_code=append_only). - Bulk-First — 200 OK always; check
results[i].status. - Registry Synonyms — multiple IDs per entity is normal;
merge_entriesreconciles (one-way, no reactivation endpoint yet). - Template Cache — 5 s TTL on "latest" resolution; explicit-version lookups cache permanently.
- Edge Types Are Stored as Templates —
usage: relationshiptemplates get extra validation,/relationships+/traverse, and dedicated reporting columns;usageis immutable; endpoints are append-only-widenable viaadd_edge_type_endpoints. versioned: false— an edge-type-only exception to PoNIF #2: updates overwrite in place, no history; requires non-emptyidentity_fields; immutable after creation.
This is the same source text the PoNIFs library doc is generated from — read that doc for the full write-up (traps, rules, and the "Compactheimer's Warning" section for AI agents recovering from context compaction); this reference only summarizes.
wip://query-assistant-prompt
Dynamic (async, server.py:764-833) — at read time it calls the same data-model-summarizing logic describe_data_model uses (_build_data_model_markdown) and embeds the live template/terminology catalog into the returned text. Falls back to a placeholder message if that call fails. Intended for direct use as the system prompt for a read-only, natural-language WIP query assistant: it states the assistant's capabilities (search, list/filter, terminology lookup, run_report_query SQL, describe_data_model), a query strategy (use search for free text, query_by_template for filtered listing, get_document to resolve reference fields, run_report_query for cross-template JOINs/aggregation, namespace-qualified Postgres table names), and explicit "don't guess / don't invent / don't modify" guardrails. No dedicated Library doc covers this resource, so it is described here in full.
Tools
97 tools total (grep -c '^@mcp\.tool()' server.py = 97), registered in the file order below, which this reference preserves as its stable order.
How tool responses/errors work — stated once, not repeated per tool below. Every tool takes its parameters as named MCP-tool arguments (not a REST path + body) and returns a JSON string (json.dumps(data, indent=2, default=str)) of whatever the wrapped WIP-service call returned, unless noted in the Notes column. Concretely, three response shapes recur:
- Single-item write/read tools (
create_document,create_terminology,add_synonym,get_terminology, …) either return the unwrapped item directly, or — for the create/update/delete-style ones — internally unwrap a 1-itemBulkResponse(client.py:255-268) and raise aBulkError(carrying a machine-readableerror_code) on a per-item error; every tool'sexcept Exception as e: return _error(e)turns that into the stringWIP error [<error_code>]: <message>(orError: <message>for a non-WIP exception) rather than propagating a raw exception to the MCP client (server.py:56-62). *_bulktools (create_documents_bulk,create_templates_bulk,create_terminologies_bulk,create_termsfor 2+ items,delete_documents_bulk) do not unwrap: they return{total, succeeded, failed, results}and the caller must checkresults[i].statusthemselves (bulk-first, stated once here per the template's instruction not to recite it per endpoint).- List/get tools return the store's raw JSON, including the platform pagination envelope (
items,total,page,page_size,pages) wherever the underlying endpoint paginates.
Where the source docstring gave a literal example call, it is reproduced under Notes; tools without one are invoked by name with the parameters listed — no example response is invented for them (per the reading list's "never invent a response" rule).
Discovery
| Tool | Params | Purpose | Notes |
|---|---|---|---|
get_wip_status() | — | Health of all five services. | Hits each service's /api/<service>/health (not the container-root /health used by orchestrator liveness probes) — see gotchas. |
describe_data_model(namespace=None) | namespace | Markdown summary of all active templates (fields, identity fields) + terminologies + query conventions, for system-prompt injection. | Paginates internally over list_templates/list_terminologies until exhausted. |
list_namespaces(include_archived=False) | include_archived | List namespaces. | |
create_namespace(prefix, description="", isolation_mode="open", deletion_mode="retain") | prefix, description, isolation_mode, deletion_mode | Create a namespace. | |
upsert_namespace(prefix, description=None, isolation_mode=None, deletion_mode=None, allowed_external_refs=None, confirm_enable_deletion=False) | as listed | Idempotent PUT-style upsert — creates on missing, patches supplied fields on existing. | Guards: the wip namespace can never go full; retain→full on an existing namespace needs confirm_enable_deletion=true (else 400); a new namespace may be created deletion_mode=full directly. |
get_namespace_stats(prefix) | prefix | Entity counts by type for a namespace. | |
delete_namespace(prefix, dry_run=True, force=False) | prefix, dry_run, force | Delete a namespace and all its data. | Destructive; requires deletion_mode=full; dry_run=True by default returns an impact report only. |
Namespace Grants
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_grants(namespace) | namespace | List permission grants on a namespace. | Requires admin on the namespace. |
create_grant(namespace, subject, subject_type, permission, expires_at=None) | `subject_type: user | api_key | group, permission: read |
revoke_grant(namespace, subject, subject_type) | as listed | Revoke a grant. |
API Key Management
| Tool | Params | Purpose | Notes |
|---|---|---|---|
create_api_key(name, owner="system", groups=None, namespaces=None, description=None, expires_at=None, grant_permission=None) | as listed | Create a runtime API key. | Returns the plaintext key once, never stored. namespaces=None = unrestricted. Without grant_permission, a namespace-scoped key can read but not write its namespaces; grant_permission='write' also provisions the matching grant. |
list_api_keys() | — | List config-file + runtime keys. | No hashes/plaintext exposed. |
revoke_api_key(name) | name | Hard-delete a runtime key. | Config-file keys cannot be revoked this way. |
Registry Entries & Synonyms
| Tool | Params | Purpose | Notes |
|---|---|---|---|
get_entry(entry_id) | entry_id | Full Registry entry: synonyms, composite keys, metadata. | |
lookup_entry(entry_id=None, namespace=None, entity_type=None, composite_key=None) | one of entry_id OR namespace+entity_type+composite_key | Look up an entry by ID or by composite key (also searches synonyms). | Errors with a usage message if neither form is supplied. |
add_synonym(target_id, synonym_namespace, synonym_entity_type, synonym_composite_key) | as listed | Add an alternative composite key that resolves to an existing entry. | For cross-namespace linking / external ID mapping. |
remove_synonym(target_id, synonym_namespace, synonym_entity_type, synonym_composite_key) | as listed | Remove a synonym. | |
merge_entries(preferred_id, deprecated_id) | preferred_id, deprecated_id | Merge two entries; the deprecated one goes inactive and its ID becomes a synonym of the preferred one. | One-way — no reactivation endpoint for the deprecated entry yet. |
Terminologies
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_terminologies(namespace=None, page=1, page_size=50) | as listed | List terminologies. | |
get_terminology(terminology_id) | terminology_id | Get by ID or value. | |
get_terminology_by_value(value, namespace=None) | value, namespace | Get by value code, case-sensitive. | |
create_terminology(value, label, namespace=None, description=None, mutable=False, on_conflict="error") | as listed | Create a terminology. | value convention UPPER_SNAKE_CASE. mutable=true implies extensible=true and terms become hard-deletable. on_conflict='validate': identical re-create → status=unchanged; drifted config → error_code=incompatible_config. |
create_terminologies_bulk(items, namespace=None, on_conflict="error") | as listed | Create many at once. | *_bulk shape — see response note above. |
update_terminology(terminology_id, label=None, description=None, mutable=None, namespace=None) | as listed | Update label/description/mutability. | mutable only changeable when term_count == 0. Errors if no field supplied. |
delete_terminology(terminology_id, force=False, hard_delete=False, namespace=None) | as listed | Delete a terminology. | Mutable terminologies are always hard-deleted regardless of the flag; immutable ones soft-delete unless hard_delete=true (needs deletion_mode=full). Blocked if terms exist unless force=true. |
restore_terminology(terminology_id, restore_terms=True, namespace=None) | as listed | Reactivate a deactivated terminology. | restore_terms=True also reactivates its inactive terms. |
Terms
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_terms(terminology_id, search=None, page=1, page_size=50) | as listed | List/search terms in a terminology. | |
get_term(term_id, namespace=None) | term_id, namespace | Get by ID, value, or synonym. | |
create_terms(terminology_id, terms, on_conflict="error") | terms: [{value, label, description?, aliases?}] | Create terms. | Single-term calls: on_conflict='validate' → unchanged/incompatible_config. Multi-term calls always skip duplicates (status=skipped) regardless of on_conflict. Example in source: create_terms("T-xxx", [{"value":"GB","label":"United Kingdom","aliases":["UK","Britain"]}, {"value":"US","label":"United States","aliases":["USA"]}]). |
validate_term_value(terminology, value) | terminology, value | Test whether a value resolves in a terminology. | |
update_term(term_id, label=None, aliases=None, description=None, sort_order=None, namespace=None) | as listed | Update a term. | aliases replaces the existing list wholesale, not merges. Errors if no field supplied. |
delete_term(term_id, hard_delete=False, namespace=None) | as listed | Delete (soft by default). | Terms in mutable terminologies are always hard-deleted. hard_delete=true on an immutable terminology needs deletion_mode=full. |
deprecate_term(term_id, reason, replaced_by_term_id=None, namespace=None) | as listed | Deprecate with a reason + optional replacement pointer; term stays queryable, flagged superseded. | Use instead of delete for historically-valid terms. |
Ontology (Relations)
| Tool | Params | Purpose | Notes |
|---|---|---|---|
get_term_hierarchy(term_id, direction="children", relation_type=None, max_depth=10, namespace=None) | `direction: children | parents | ancestors |
create_term_relations(term_relations, namespace=None) | term_relations: [{source_term_id, target_term_id, relation_type}] | Create ontology relations. | relation_type ∈ {is_a, part_of, has_part, regulates, positively_regulates, negatively_regulates} (plus custom types via the _ONTOLOGY_RELATIONSHIP_TYPES terminology, per wip://conventions). Example: create_term_relations([{"source_term_id":"ALZHEIMERS_DISEASE","relation_type":"is_a","target_term_id":"NEUROLOGY"}]). |
list_term_relations(term_id, direction="outgoing", relation_type=None, namespace=None, page=1, page_size=50) | `direction: outgoing | incoming | both` |
delete_term_relations(term_relations, namespace=None, hard_delete=False) | same shape as create | Delete relations. |
Templates
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_templates(namespace=None, status=None, latest_only=True, page=1, page_size=50) | `status: active | inactive | draft` |
get_template(template_id, version=None, namespace=None) | as listed | Get a template with inherited fields resolved. | version=None = latest. |
get_template_by_value(value, namespace=None) | value, namespace | Get by value code. | |
get_template_raw(template_id, namespace=None) | as listed | Get without inheritance resolution — only fields defined directly on this template. | |
create_template(template: dict, namespace=None) | template: {value, label, fields, description?, extends?, extends_version?, identity_fields?, header_fields?, status?} | Create a template. | Field naming: mandatory (not required), terminology_ref/template_ref (not *_id). status='draft' skips reference validation (for circular deps). Updating an existing value creates a new version — see update_template. On an "already exists" error, the tool appends a hint to use update_template instead of retrying create_template. |
create_templates_bulk(templates, namespace=None) | as listed | Create many templates at once. | Pairs with draft mode + activate_template for circular dependencies. |
create_edge_type(value, label, source_templates, target_templates, fields, namespace=None, description=None, versioned=True, identity_fields=None, rules=None) | as listed | Create an edge type — a template with usage: relationship — the documented happy path over hand-rolling create_template with usage set manually. | Performs its own pre-flight validation in Python before calling template-store: source_templates/target_templates non-empty; fields must include source_ref and target_ref, each type=reference, reference_type=document, with target_templates matching the corresponding template-level list. On failure, returns the plain string "Edge-type contract validation failed:\n - ..." — not the usual _error()/error_code shape. identity_fields defaults to [source_ref, target_ref] when omitted (pass [] explicitly to opt into true append-only). usage, source_templates/target_templates (widen-only via add_edge_type_endpoints), and versioned are immutable after creation. |
update_template(template_id, updates: dict, namespace=None) | updates keys: label, description, fields (full replace), identity_fields, header_fields, extends, rules, metadata, reporting | Update by creating a new version; template_id stable, only the version increments. | Returns template_id, value, version (new), is_new_version, previous_version. Existing documents keep validating against the version they were created with. |
activate_template(template_id, namespace=None, dry_run=False) | as listed | Activate a draft template — validates all references, cascades to referenced drafts. | All-or-nothing: any failure in the chain activates none. |
deactivate_template(template_id, version=None, force=False, hard_delete=False, namespace=None) | as listed | Delete a version (soft by default). | Blocked if other templates extends it. force=true overrides the "documents exist" block. hard_delete=true needs deletion_mode=full. |
reactivate_template(template_id, version, namespace=None) | version required | Restore an inactive version to active — inverse of deactivate_template. | Idempotent on an already-active version; rejects a draft version. |
add_edge_type_endpoints(template_id, add_source_templates=None, add_target_templates=None, namespace=None) | as listed | Additively widen an edge type's allowed source/target templates. | Append-only (never removes); applied in place with no new version; preserves every existing edge; idempotent; each new endpoint must be a real template. |
get_template_dependencies(template_id, namespace=None) | template_id, namespace | What depends on a template: child templates + documents. | |
get_template_versions(template_value=None, template_id=None, namespace=None) | one of template_value or template_id | List all versions, newest first. | |
validate_template(template_id, namespace=None) | template_id, namespace | Check terminology_ref/extends references resolve to active entities. | Useful pre-activate_template. |
Documents
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_documents(template_value=None, namespace=None, template_id=None, status=None, latest_only=True, page=1, page_size=20) | as listed | List documents, optionally filtered by template. | |
get_document(document_id, version=None) | as listed | Get a document. | version=None = latest. |
validate_document(template_id, data: dict, namespace=None) | as listed | Validate data against a template without saving. | Returns valid, errors, warnings, identity_hash (if valid), template_version. Useful for computing an identity hash pre-submission. |
validate_documents(template_id, items: list[dict], namespace=None, template_version=None) | as listed | Bulk, side-effect-free validation of many items against one template. | Returns {"results": [...]} in input order, each shaped like validate_document's result. An unresolvable template_id fails the whole call (404) since the batch is single-template; a single invalid item is reported via that item's valid=false, not a call failure. |
create_document(document: dict, namespace=None) | document: {template_id, template_version (recommended), data} | Create (or, on identity match, version) a document. | Term fields: submit the human-readable value; WIP resolves to term_id (clear error on resolution failure). Without template_version, resolves "latest active" — may not be the version you expect if multiple are active. Example in source: create_document({"template_id":"TPL-xxx","template_version":1,"namespace":"wip","data":{"name":"Jane Doe","email":"jane@example.com","gender":"Female"}}). |
create_documents_bulk(documents: list[dict], namespace=None) | as listed | Create many documents. | *_bulk shape. |
query_documents(filters: dict) | filters: {template_id, namespace?, status?, page?, page_size?, filters: [{field, operator, value}]} | Low-level document query. | filters[].field needs the data. prefix (e.g. data.country). Operators: eq, ne, gt, gte, lt, lte, in, nin, exists, regex. Prefer query_by_template for the auto-prefixed convenience version. |
get_document_relationships(document_id, direction="both", template=None, namespace=None, active_only=True, page=1, page_size=50) | `direction: incoming | outgoing | both` |
traverse_documents(document_id, depth=1, types=None, direction="outgoing", namespace=None) | depth: 1..10 | N-hop BFS graph traversal via relationship documents. | Returns a flat node list with depth, path, via_relationship. Visited nodes are skipped (cycles terminate). Capped at depth=10 and 1000 nodes; sets truncated=true if a cap fires. Edges are implicit — call get_document_relationships on individual nodes for edge data. |
get_document_versions(document_id) | document_id | List all versions of a document, with latest version number + total count. | |
archive_document(document_id) | document_id | Soft-delete (status → inactive). | |
update_document(document_id, patch: dict, if_match=None) | as listed | Apply an RFC 7396 JSON Merge Patch to data, creating version N+1 (previous becomes inactive; publishes NATS DOCUMENT_UPDATED, which reporting-sync consumes to refresh the reporting layer). | Objects deep-merge; arrays are replaced wholesale, not merged element-wise; null deletes a key. Error codes: identity_field_change (cannot PATCH identity fields — create a new document instead), append_only (template has no identity_fields), archived (unarchive first), not_found (deleted/nonexistent), concurrency_conflict (if_match mismatch). The merged document must still validate against the template version the document was originally created with — PATCH does not migrate to a newer template version. |
migrate_documents(template_id, from_version, to_version, dry_run=True, namespace=None) | as listed | Re-pin a cohort of documents from one template version to another. | dry_run=True by default — reports per-document readiness without writing (validation_failed details name e.g. unknown_field for a removed field or required for a newly-mandatory one). Identity-preserving only: both versions must declare the same identity_fields, else the whole call is rejected (identity_fields_changed — a fork needs new documents, not a migrate). On apply, creates a new version (or overwrites in place for versioned: false templates) keeping the same document_id/identity_hash. Source version may be inactive (frozen); target must be active. See gotcha below — this tool is a write despite living in a read-adjacent-sounding name. |
delete_document(document_id, hard_delete=False, version=None) | as listed | Delete (soft by default). | hard_delete=true needs deletion_mode=full. version (hard-delete only) defaults to all versions. |
delete_documents_bulk(document_ids, hard_delete=False, namespace=None) | as listed | Delete many documents. | *_bulk shape; hard_delete applies uniformly to every ID. |
Import/Export (Terminology)
| Tool | Params | Purpose | Notes |
|---|---|---|---|
export_terminology(terminology_id, format="json", include_relations=True, namespace=None) | `format: json | csv` | Export a terminology with its terms (and optionally ontology relations). |
| `import_terminology(data: dict | list, format="json", skip_duplicates=True, update_existing=False, namespace=None)` | as listed | Import a terminology + terms from JSON data. |
Table View (Denormalized Export)
| Tool | Params | Purpose | Notes |
|---|---|---|---|
get_table_view(template_value, status=None, page=1, page_size=100, namespace=None) | as listed | Flattened, spreadsheet-like document view for a template. | Arrays are cross-product expanded into rows, capped at 1000 rows. |
export_table_csv(template_value, status=None, include_metadata=True, namespace=None) | as listed | Export a template's documents as CSV. | Returns raw CSV, but this MCP tool truncates the response to the first ~100 data rows (102 lines incl. header) if the export is larger, appending "... truncated (N total lines)". |
Search & Reporting
| Tool | Params | Purpose | Notes |
|---|---|---|---|
search(query, types=None, namespace=None, page=1, page_size=20, template=None, mode="auto", include_inactive=False, snippet_format="html") | as listed | Unified search across all WIP entity types, via reporting-sync. | mode: auto (default, FTS where a field declared full_text_indexed: true, ILIKE substring fallback elsewhere) / fts (force FTS, skips unindexed tables) / substring (legacy ILIKE, no ranking, works everywhere). types filters entity type (terminology, term, template, document, file); template further restricts document hits to one template value. Response groups hits per entity type, each with its own items/total/page/page_size/pages envelope. include_inactive=False by default (PoNIF #1 — retired ≠ deleted). FTS hits carry score (ts_rank) + snippet (ts_headline, HTML or plain per snippet_format); substring/non-document hits have neither. |
search_registry(query, namespace=None, entity_type=None) | `entity_type: terminologies | terms | templates |
Files
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_files(namespace=None, page=1, page_size=20) | as listed | List files (MinIO-backed binary storage). | |
get_file_metadata(file_id) | file_id | Metadata only, not content. | |
upload_file(file_path, namespace=None, description=None, tags=None, category=None) | as listed | Upload a local file. | file_path must be an absolute local path; content-type is guessed via mimetypes; tags is a comma-separated string split into a list. Returns file_id (UUID7) + metadata. |
delete_file(file_id, force=False) | as listed | Soft-delete (status → inactive). | Blocked if documents reference the file unless force=true. |
hard_delete_file(file_id) | file_id | Permanently remove from MinIO, reclaiming storage. | File must already be soft-deleted; irreversible. |
get_file_documents(file_id) | file_id | Which documents reference a file, and at which field path. |
Template-Aware Query
| Tool | Params | Purpose | Notes |
|---|---|---|---|
get_template_fields(template_value, namespace=None) | template_value, namespace | Clean field summary (name/type/mandatory/references) + template_id, for use before querying/creating documents. | |
query_by_template(template_value, field_filters=None, status=None, page=1, page_size=20, namespace=None) | field_filters: [{field, operator, value}] | Query documents by template value with easy field filtering. | Resolves template_value → template_id automatically. Field names auto-prefixed with data. unless already prefixed or one of the reserved top-level names (document_id, template_id, namespace, status, version, created_at, updated_at). Same operator set as query_documents. |
Reporting & SQL Query
| Tool | Params | Purpose | Notes |
|---|---|---|---|
list_report_tables(table_name=None) | table_name | Discover reporting tables. | Omit table_name for a compact summary (name, row_count, column_count) of all tables; pass it for full column detail (name, type, nullable) on one table. |
run_report_query(sql, params=None, max_rows=1000, namespace=None) | as listed | Execute a read-only SQL SELECT against the PostgreSQL reporting database, for cross-template JOINs/aggregation/analytics the document API can't do. | Each namespace is its own Postgres schema: a table is "<namespace>"."doc_<value>". Term fields expose two columns: {field} (value) + {field}_term_id. namespace= sets search_path so unqualified names resolve there; omit and schema-qualify for cross-namespace queries. Example: run_report_query("SELECT name, country FROM doc_patient WHERE country = $1", ["CH"], namespace="clinic-a"). |
export_report_csv(table=None, sql=None, params=None) | exactly one of table/sql | Export reporting data as CSV — a whole table, or a filtered/joined query result. | table mode is restricted to doc_* and metadata tables. Same first-~100-rows truncation as export_table_csv. |
Import (CSV/XLSX)
| Tool | Params | Purpose | Notes |
|---|---|---|---|
import_documents_csv(file_path, template_value, column_mapping=None, namespace=None, skip_errors=True) | as listed | Import documents from a local CSV/XLSX file into a template, bulk-creating documents. | Without column_mapping, auto-maps CSV headers to field names by case-insensitive match (spaces → underscores); errors listing both header and field-name sets if nothing auto-maps. skip_errors=True skips rows that fail validation rather than aborting. |
Event Replay
| Tool | Params | Purpose | Notes |
|---|---|---|---|
start_replay(template_value=None, template_id=None, namespace=None, throttle_ms=10, batch_size=100) | as listed | Republish stored documents as NATS events (a dedicated stream, metadata.replay=true) — for onboarding new consumers or backfilling. | throttle_ms 0-5000; batch_size 10-1000; omitting both template filters replays the whole (active) namespace. |
get_replay_status(session_id) | session_id | Published count, total, status of a replay session. | |
cancel_replay(session_id) | session_id | Cancel a session and delete its NATS stream. | |
pause_replay(session_id) / resume_replay(session_id) | session_id | Pause/resume a running session. | |
start_backup(namespace=None, include_files=False, include_inactive=False, skip_documents=False, skip_closure=False, skip_synonyms=False, latest_only=False, template_prefixes=None, dry_run=False) | as listed | Start a namespace backup; returns the initial BackupJobSnapshot. Runs in the background — poll get_backup_job until complete/failed, then download_backup_archive. | In-source WARNING: include_files=true is a v1.0-documented-unsafe path — the archive writer buffers all blob bytes in RAM and can OOM the document-store container; leave it false until a streaming archive path ships. |
start_restore(namespace, archive_path, mode="restore", target_namespace=None, register_synonyms=False, skip_documents=False, skip_files=False, batch_size=50, continue_on_error=False, dry_run=False) | as listed | Restore a namespace from a local .zip archive (uploaded multipart); returns the initial BackupJobSnapshot. | In-source GOTCHA: mode='restore' writes back to the source namespace embedded in the archive and ignores target_namespace — use only to restore into the same namespace the archive came from. mode='fresh' generates new IDs and honors target_namespace — use for a round-trip into a new namespace. register_synonyms (original IDs as synonyms of new IDs) only makes sense with mode='fresh'. |
get_backup_job(job_id) | job_id | Poll a backup/restore job's state: status, phase, percent, message, archive_size (when complete). | |
list_backup_jobs(namespace=None, status=None, limit=50) | `status: pending | running | complete |
download_backup_archive(job_id, dest_path) | job_id, dest_path | Stream a completed backup's .zip to a local path. | Job must be a backup job in complete status. Parent directories of dest_path are auto-created. |
delete_backup_job(job_id) | job_id | Delete a backup/restore job and its archive file. | Cannot delete a running job. |
get_sync_status() | — | Reporting-sync service status: NATS connection, PostgreSQL connection, events processed/failed, tables managed. | Useful for diagnosing sync lag/failures. |
Store-specific gotchas
- Single-item unwrap vs.
*_bulkenvelope. A single-item write tool (e.g.create_document) raises internally and returns a formattedWIP error [<error_code>]: <message>string on a per-item failure — it never silently returns a "200-shaped" success-looking payload for a failed item. The*_bulksiblings do the opposite: they always return{total, succeeded, failed, results}, and a caller that doesn't checkresults[i].statuscan miss failures inside an apparently-successful call. Know which family you're calling. WIP_MCP_MODE=readonlystrips 47 of the 97 tools (WRITE_TOOLS,server.py:3961-4022) at import time viamcp._tool_manager.remove_tool(...)and rewrites the server'sinstructionstext to say so — but see the Code findings section below: this allowlist has a gap.- Tool descriptions/schemas are patched at import time.
_patch_tool_schemas()(server.py:3893-3954) overwrites each registered tool's description and dict-parameter JSON schema from_generated_schemas.py(auto-generated from the WIP OpenAPI specs) plus atools.yamlconfig file, if both are present. What an actual MCP client sees for a tool can therefore be richer than the raw docstring documented above — see the GAP flag ontools.yamlbelow. create_edge_typefails differently from every other tool. Its pre-flight validation (source/target template lists non-empty;source_ref/target_reffield shape) runs in Python before any HTTP call and returns a plain"Edge-type contract validation failed:\n - ..."string on failure — not the_error()/error_codeshape every other tool uses. Code that branches onerror_codewill not catch this path.- Namespace resolution differs by tool family. Most read/query tools treat an omitted
namespaceas "search all namespaces"; most write tools resolve it viaWipClient._ns(), which falls back toWIP_MCP_DEFAULT_NAMESPACEand raises if neither is supplied. Single-namespace API keys auto-derive namespace server-side; multi-namespace keys must pass it explicitly on every call (seewip://conventions). get_wip_statuschecks a different/healththan the orchestrator does. It calls each service's/api/<service>/health, not the container-root/healththat podman/k8s healthchecks use — a service can be failing one and passing the other.- CSV exports are truncated by the MCP layer, not the store.
export_table_csvandexport_report_csvboth cut off at roughly the first 100 data rows client-side ("to avoid overwhelming the AI"); the full export exists only via the underlying document-store/reporting-sync HTTP endpoint, not through these MCP tools. - HTTP/SSE transport can start unauthenticated. If neither
API_KEYnorWIP_AUTH_LEGACY_API_KEYis set when launching with--http/--sse, the server prints a stderr warning and starts anyway —ApiKeyMiddlewareis never installed, so anyone with network access gets full CRUD through the AI-facing tool surface.
See also
API Conventions; PoNIFs; Auth & Access; Routing & Ingress; Document-Store API; Registry API; Template-Store API; Def-Store API; Reporting & Analytics (REST + SQL layer).
