oto_project
Projects (organization layer). op=create (name, optional brief_md; owner_type user|org + owner_id for a team project) / list (ORG-SCOPED: the ACTIVE org's projects, its teams' + projects shared with it or with your teams in it + YOUR personal projects created in it (visible to you only) — a personal project is listed ONLY in the org where it was created; your PERSONAL org also lists those shared with YOU as a person; pass _org=<id> to see another org's; scope="me" lists only the projects shared with you as a person, in any org; every response echoes the effective org in _org. An INDEX: names and brief_md_length, NOT the briefs — read one with op=get, or pass fields=["*"] for whole records; include=["links"] adds every project's links in the same call, include=["audit"] its LIGHT audit, flagged light: true) / list_templates (published MODEL projects you can copy, from the ACTIVE org and the platform library) / get (project + its links + an audit of those links: dead_links / unbound_slots / unresolvable_connectors (a linked connector no org credential would resolve — org-owned projects only, always empty otherwise) / inert_procedures — a linked entity that no longer resolves surfaces HERE, act on it) / update (name, icon = an emoji shown in the lists and headers ("" clears it), folder = a display label the lists group projects under ("" clears it), position = the project's rank within its folder in lists (lower first, fractions allowed; clear_position=true unranks it), brief_md, is_template = publish/unpublish as a copyable model, excluded_url_prefixes = URL prefixes such as linkedin.com/in/ that search tools drop and extraction tools refuse under this project — a whole host must be written host/*, [] clears, mcp_instructions_md = the prose the published endpoint serves its recipients, fixed WITHOUT republishing — slug, access and tools stay as they are; owner/admin only) / copy (deep-copy a project you can read — its own or a model — into a NEW project in your active org: brief + doc tree + links + raw files; a tableau link stays a POINTER to the same namespace by default (config.provision absent/shared), but with config.provision=empty|seeded it is PROVISIONED — a FRESH namespace (same schema, rows only if seeded) so each copy gets its own isolated table (e.g. a campaign template's lead pool). A shared tableau owned by ANOTHER org is re-provisioned EMPTY (never a pointer to the source's private data), and links whose namespace no longer resolves are skipped — both surfaced in the response warnings. Pass project_id = source + name = target) / handoff (a copy-paste « resume in Claude » blob that pre-writes the per-call _project= token for this project) / archive (hides it from every list, destroys nothing; the response says what became unreachable — pages, linked procedures, links, brief — and a project with a non-empty brief or a linked procedure needs confirm=true, else 409 confirm_required) / unarchive (puts an archived project back; was_archived_at echoes what it undid; find archived ones with op=list archived=true) / link & unlink (attach an entity: target_type tableau|procedure|connecteur + target_ref = its id/slug/name, optional label + optional role = why this entity belongs to the project + optional config = the entity's PRE-MADE per-project override; for a connecteur: {identity_id?, instructions_md?} = which account to act as + prose instructions to apply (e.g. 'only filter agreements by the mutuelle theme'), or instance_ref (a ref from oto_instance op=list, ADR 0038 B5) to bind EXACTLY that credential — calls carrying this project's token then resolve it hard, no fallback; for a tableau: {provision?: shared|empty|seeded} = how a project copy treats it (empty/seeded = each copy gets its own fresh table). Optional slot = the SLOT NAME this link BINDS for the project (ADR 0035): procedures declare required entities as slots and reference them slot:name in their prose — the project maps each name to a concrete entity via its links. Slot names are a PROJECT-wide vocabulary (unique per project → 409 slot_taken; two linked procedures sharing sortie share the binding). Re-linking preserves every field you omit — label, role, config and slot; pass a value to change one. link says WHAT IT DID in link_status: created (the binding did not exist), unchanged (it was already there and this call rewrote nothing) or updated (it existed and this call changed it — changed_fields then lists which of label/role/slot/config/target_ref moved). Re-running a link is safe and idempotent, so a caller told to ENSURE a resource is attached must read link_status — not ok — to know whether it actually acted. unlink returns removed = how many bindings it actually took out, and REFUSES (link_not_found) when it matched none — it never answers ok on a link it did not find. rank_link (target_type + target_ref as op=get renders it, + optional identity_ref) sets ONE link's rank among the project's links of its type in lists — position (lower first, fractions allowed) or clear_position=true; it changes nothing else: no updated_at, no activity line, label/role/slot/config untouched; link_not_found when no such link. Give the target_ref as op=get renders it: an older link may still carry the NAME of its tableau (or the SLUG of its procedure) instead of the id. link and unlink both recognize either spelling: link rewrites that older link to the id (rewritten_from = the old spelling) instead of adding a second one, unlink takes back every spelling. get/link return each link's role + slot + config + position (its rank, set by rank_link; null = unranked) + a derived cross_project flag (the same entity is linked by another project → avoid brutal edits / ask); a tableau link also returns its resolved datastore (the NAME, a label) and datastore_id (the IDENTIFIER, resolved server-side in the PROJECT OWNER's scope) — address THIS project's table with datastore_id in the data_* tools, never by hardcoding a name: several tables can carry one name, and at equal name resolution prefers the CALLER's own personal table. No datastore_id = this link does not resolve to a single table here (datastore_ambigu: true = its NAME designates several tables in the owner's scope) — say so instead of guessing. EVERY project carries url — the web address to OPEN it, in the reader's own product; hand it over as-is when asked "where is it?", never rebuild one from a pattern (null = that reader's product has no such view). Share & transfer go through oto_resource (resource_type='project', ADR 0030 owned resource) — this includes RE-PARENTING a project in place (same id, links, runs preserved): op=transfer new_owner_group= hands it to a TEAM so the project and its connector credentials sit at the SAME level (the team's secrets then resolve when you open it), new_owner_org= to an org, new_owner_email to a user. (op=update only changes name/icon/folder/position/brief_md/is_template — never the owner; op=copy makes a NEW id.) inventory = the project's DERIVED surface (union of the linked procedures' tool: refs + tools actually used by the project's runs, plus connectors from links & declared slots) — never retype a tool list: derive, then curate. runs (optional target_ref = a linked procedure's stable id) = the project's recent runs (label/guide/outcome), filtered to that procedure when given. OMIT project_id on op=runs and you get YOUR OWN still-open runs instead, each with its run_id — that is how you find a run you opened and lost the id of, so you can finally close it with run_finish. Across every org, since a run you cannot find is usually one you opened elsewhere. lint (optional stale_days, default 90) = health of this project's pages: stale (untouched since), empty (trivial body), duplicate_titles (likely merges). publish_mcp (mcp_slug + mcp_access anonymous|secret|org + mcp_tools = the fixed tool allowlist) publishes the project as a dedicated MCP endpoint <mcp_slug>.mcp.oto.cx/mcp, the toolset served under the OWNER ORG's credentials — anonymous = no login + LISTED in the public directory; secret = no login but UNLISTED, the slug is server-generated & unguessable (a secret URL; mcp_slug is an optional readable prefix); org = Logto JWT + pins the org. For anonymous/secret, tools that aren't credential-less or resolvable for the org are published anyway but FAIL cleanly at call time — they come back in mcp_unresolvable_tools (configure an org key or drop them). mcp_expose_datastore (SECRET only) opts the data_* tools in: they then act under the OWNER ORG's authority (read/write the org's namespaces) without a login — off by default (the datastore stays private); refused on anonymous/org. Publishing takes governing the project AND being a member of the org that owns it (the parent org of an owning team; the owner themself for a personal project) — a manager from outside is refused 403 publish_requires_membership. unpublish_mcp removes it, open to whoever governs. get returns mcp_slug/mcp_access/mcp_tools/mcp_expose_datastore/mcp_url.
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"op": "<op>"
},
"name": "oto_project"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| op | Yes | To CHANGE THE OWNER of an existing project (e.g. personal → org) WITHOUT duplicating it or losing its history (docs, activity, runs stay on the SAME project_id): no op here does that — `op=copy` DUPLICATES into a new project. Use `oto_resource(op="transfer", resource_type="project", resource_id=<id>, new_owner_org=<org_id>, cascade=true)` (ADR 0030): same id, and a former personal owner keeps write access. `cascade` is NOT optional detail — it defaults to false, and without it the project moves ALONE while its linked entities stay behind, silently. With `cascade=true`: linked tableaux you govern are transferred too, a linked procedure is COPIED to the new owner org (the original stays with its owner) and connectors come back `action_required` — the recipient plugs in their own credential. | |
| _org | No | ||
| icon | No | update: emoji shown in lists and headers. `""` clears it. | |
| name | No | ||
| role | No | ||
| slot | No | ||
| depth | No | ||
| label | No | ||
| scope | No | list only: `org` (default) = the projects of the organization you act in, of its teams, those shared with it or with your teams in it, and your personal projects created in it (visible to you only) — a personal project is listed ONLY in the organization where it was created; your PERSONAL org also lists everything shared with you as a person; `me` = only the projects shared with YOU as a person, whatever their organization — served in any organization. | |
| config | No | ||
| fields | No | list/list_templates: output projection. Omitted returns the INDEX (no briefs); `["*"]` returns whole records; a list of names picks columns. | |
| folder | No | update: the folder the project is shown under in lists — a free label (≤ 80 characters), shared by every project that carries the same one. Display only: no hierarchy, no access rule. `""` clears it. | |
| _run_id | No | OBLIGATOIRE à chaque appel dès `run_start` ou `data_claim_next` : sans lui, l'écriture sur une ligne réservée est refusée. Hérité seulement si le serveur tient un run actif — n'y compte pas. | |
| confirm | No | archive only: required (`true`) when the project carries a non-empty brief or a linked procedure — without it the archive is refused (409 `confirm_required`) and the response says what would become unreachable. | |
| include | No | get: `spine` (page tree) and/or `procedures` (bodies of linked procedures). list: `links` adds each project's links, as op=get serves them — to find which projects link a target in ONE call instead of one op=get per project; `audit` adds each project's LIGHT audit — op=get's keys plus `light: true`: dead tableaux and connectors missing from the registry only; dead procedures and unresolvable connectors are checked by op=get alone. | |
| archived | No | list only: `true` lists the ARCHIVED projects (yours and those of the orgs and teams you see) instead of the live ones — to find one and unarchive it. | |
| brief_md | No | ||
| from_doc | No | ||
| mcp_slug | No | ||
| owner_id | No | ||
| position | No | update: the project's rank among the projects of its folder in lists — lower first, fractions allowed (place a project between two others with the midpoint). Unranked projects come after the ranked ones. Display only; setting it alone does not change `updated_at`. rank_link: the same, for ONE link among the project's links of its type. | |
| mcp_tools | No | ||
| mcp_access | No | ||
| owner_type | No | create/copy only (default `user`). op=update refuses it: move a project with oto_resource op=transfer resource_type=project. | |
| project_id | No | OMIT it on op=runs and you get YOUR OWN still-open runs instead, across every org, each with its `run_id` — that is how you find a run whose id you lost. | |
| stale_days | No | ||
| target_ref | No | ||
| is_template | No | ||
| target_type | No | ||
| identity_ref | No | ||
| instance_ref | No | ||
| clear_position | No | update / rank_link: `true` takes the project (the link) out of the ranking — `position` back to null, after the ranked ones. Not together with `position`. | |
| mcp_expose_docs | No | ||
| mcp_instructions_md | No | ||
| mcp_expose_datastore | No | ||
| excluded_url_prefixes | No | update: `host/path/` patterns search tools drop and extraction tools refuse under this project. A whole host must be written `host/*` — a bare host is refused. `[]` clears the list. | |
| mcp_expose_datastore_write | No |