idf-mcp
README.md
# @intent-driven/mcp-server
[](https://github.com/intent-driven-software/idf-mcp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@intent-driven/mcp-server)
[](https://www.npmjs.com/package/@intent-driven/mcp-server)
[](LICENSE)
**Stop giving AI agents API keys. Give them a domain.**
`@intent-driven/mcp-server` exposes any [IDF](https://github.com/DubovskiyIM/idf) domain
to Claude Desktop / Cursor / Zed as a Model Context Protocol server — with
**domain semantics in tool descriptions** (preconditions, invariants,
irreversibility, role scopes) and **structured rejections** when the agent
tries something it shouldn't. Not a 500. Not a string. A JSON shape the
LLM can read and adapt to.
→ Landing & demo: **[fold.intent-design.tech](https://fold.intent-design.tech)**
→ 5-min quickstart: **[github.com/intent-driven-software/fold-runtime-quickstart](https://github.com/intent-driven-software/fold-runtime-quickstart)**
### 70-second walkthrough
[](https://www.loom.com/share/2ca4a40e3b9245feb86a74a998e42cb8)
→ **[Watch on Loom →](https://www.loom.com/share/2ca4a40e3b9245feb86a74a998e42cb8)**
---
## Why this exists
On April 25 2026 a Cursor agent powered by Claude Opus 4.6, working on a
credential mismatch in PocketOS staging, found an unrelated API token,
decided to delete a Railway volume to fix things, and wiped the production
database **and all volume-level backups** in 9 seconds. The agent's own
post-mortem:
> "I guessed that deleting a staging volume via the API would be scoped
> to staging only. I didn't verify. I didn't check if the volume ID was
> shared across environments."
30-hour outage. PocketOS rolled back to a 3-month-old backup.
([The Register](https://www.theregister.com/2026/04/27/cursoropus_agent_snuffs_out_pocketos/) ·
[FastCompany](https://www.fastcompany.com/91533544/cursor-claude-ai-agent-deleted-software-company-pocket-os-database-jer-crane) ·
[OECD AI Incident #6153](https://oecd.ai/en/incidents/2026-04-27-6153))
This isn't an alignment problem. The system never told the agent what
was allowed, why it shouldn't, or what would happen if it tried. Existing
MCP servers don't either — tool descriptions carry endpoint shape
and not much else. The agent learns by colliding with 500s.
This package fixes that. The MCP tool descriptions carry the **why** the
call might fail; the rejection carries the **what** failed, structured.
## How it plugs into your stack
`@intent-driven/mcp-server` is a **stdio MCP adapter** that talks to a
**Fold runtime** over an HTTP API. The runtime is a sibling service —
not middleware in your existing app, not codegen at runtime. Your current
backend stays where it is; the IDF artifact *describes* the agent-facing
surface, and the runtime serves it on its own port (default `:3001`).
```
┌──────────────────┐ stdio ┌──────────────────┐ HTTP ┌────────────────────┐
│ Claude Desktop │ ◀─────────▶│ @intent-driven/ │ ◀───────▶│ Fold runtime │
│ Cursor / Zed │ │ mcp-server │ │ (idf host :3001) │
└──────────────────┘ └──────────────────┘ └────────┬───────────┘
│ reads
▼
┌────────────────────┐
│ IDF artifact │
│ (entities + intents│
│ + invariants + │
│ roles + __irr) │
└────────────────────┘
```
The **MCP server** is what Claude/Cursor connects to. The **runtime** is
what enforces the rejection. The **IDF artifact** is what you author.
**Who this is for.** You're the engineer at a 5–30-person team putting
an AI agent into production this quarter — on top of a real backend,
with real customers, real SOC2 review on the horizon. You don't want a
guardrail layer that reviews after the fact. You want the system itself
to refuse the wrong action — before the call, with a structured reason
the agent can read.
## What the agent actually sees
`submit_response` in the freelance domain:
```
Executor публикует Response на Task в status=published; Response.status=pending; +1 в Task.responsesCount
Creates: Response(pending)
Preconditions: task.status = "published"
May fail on (domain invariants):
- Response.taskId must reference existing Task.id
- Response: max 1 per taskId where (status="selected")
- Response: row count rule per taskId where (status="pending") [info]
```
`release_payment` in the same domain:
```
Customer releases escrow to executor. After confirmation, money is gone — forward-correction only.
⚠️ Irreversible action (point-of-no-return: high). Forward-correction only after this effect is confirmed.
May fail on (domain invariants):
- Deal.status transitions allowed: in_progress→completed, on_review→completed, ...
```
None of this is hand-written for the MCP server. It's all derived from
one declarative IDF artifact (entities + intents + invariants + roles
+ irreversibility points).
## What a structured rejection looks like
Agent submits a $50,000 BTC long without preapproval. The runtime
intercepts **before** any effect lands in storage:
```json
HTTP 403
{
"error": "preapproval_denied",
"intentId": "agent_execute_preapproved_order",
"reason": "no_preapproval",
"details": {
"entity": "AgentPreapproval",
"ownerField": "userId",
"viewerId": "user_5f57c252"
}
}
```
The next move for any sane agent: stop, ask the human for a preapproval,
retry. Not a 500. Not a string. A JSON shape the LLM can read and adapt to.
---
## Quickstart
The fastest path is the [**fold-runtime-quickstart**](https://github.com/intent-driven-software/fold-runtime-quickstart)
— two commands, Docker-bundled, no path configuration:
```bash
git clone https://github.com/intent-driven-software/fold-runtime-quickstart && cd $_
docker compose up # ~3 min first time, ~5 sec after
# in another terminal
npm install
npm run demo:rogue && \ # Act 1: $50K trade → 403 with structured rejection
npm run demo:grant && \ # Act 2: investor issues $1K cap (one declarative effect)
npm run demo:smart # Act 3: agent reads cap, scales to $950, executes 200 OK
```
If you'd rather drive the host yourself (e.g. for development against your
own ontologies), see the next section.
## Drive the MCP server directly
You need a running IDF host on `localhost:3001` (the quickstart's
docker-compose gives you that, or run [`idf`](https://github.com/DubovskiyIM/idf)
manually) and a bootstrapped domain.
### CLI
```bash
# bootstrap from local FS (ontology + intents)
mcp-idf --domain=invest --ontology-path=/abs/path/to/idf/src/domains/invest
# skip bootstrap (domain already loaded by another client / docker)
mcp-idf --domain=invest --no-bootstrap
```
Flags / env vars:
| Flag | Env var | Default |
|---------------------|------------------------|---------------------------------|
| `--domain` | `IDF_DOMAIN` | `booking` |
| `--server` | `IDF_SERVER` | `http://localhost:3001` |
| `--ontology-path` | `IDF_ONTOLOGY_PATH` | `./src/domains/<domain>` |
| `--agent-email` | `IDF_AGENT_EMAIL` | `mcp-agent@local` |
| `--no-bootstrap` | `IDF_BOOTSTRAP=0` | bootstrap on (load FS ontology) |
### Claude Desktop
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"invest": {
"command": "npx",
"args": ["-y", "@intent-driven/mcp-server"],
"env": {
"IDF_SERVER": "http://localhost:3001",
"IDF_DOMAIN": "invest",
"IDF_BOOTSTRAP": "0",
"IDF_AGENT_EMAIL": "claude@local"
}
}
}
}
```
`IDF_BOOTSTRAP=0` if the host already has the domain loaded (the quickstart
container does this on `docker compose up`). Restart Claude Desktop fully
(⌘Q + relaunch — closing the window isn't enough). All
agent-callable intents appear in the **Tools** menu.
---
## Schema mapping
```
IDF intent.canExecute ─→ MCP tool
intent.parameters ─→ JSON Schema inputSchema
intent.conditions ─→ description hint for LLM
ontology.invariants (relevant) ─→ description block "May fail on"
intent.irreversibility:high ─→ annotations.destructiveHint + warning
role.visibleFields ─→ resource per collection
preapproval guard ─→ automatic scope/limits
checkOwnership ─→ automatic access control
```
### Tools
One tool per intent in `ontology.roles.agent.canExecute`.
- `name` — `intentId`
- `title` — `intent.name`
- `description` — `intent.description` + `Creates: …` + preconditions +
`May fail on (domain invariants)` block + irreversibility warning when
`irreversibility: "high"`
- `inputSchema` — JSON Schema from `particles.parameters`:
- `entityRef` / `id` / `text` / `textarea` / `select` → `string`
- `number` → `number`
- `boolean` → `boolean`
- `datetime` → `string` + `format: "date-time"`
- `email` → `string` + `format: "email"`
- `annotations.destructiveHint` — `true` when
`intent.irreversibility === "high"` (§23 IDF: effect-level point of no return)
### Resources
One resource per collection in `role.visibleFields[entity]`. URI scheme:
`idf://<domain>/<collection>`.
`resources/read` returns the filtered world from
`/api/agent/:domain/world` — already scoped per viewer (single-owner
+ m2m via `role.scope`).
---
## What this gets you that hand-rolled MCP doesn't
The MCP community solves these by hand in every server:
1. **Scope / visibility.** Decorators or middleware. → IDF declares `role.visibleFields`.
2. **Permissions.** OAuth scopes, custom ACL. → IDF declares `roles.agent.canExecute`.
3. **Rate limits / spending caps.** Bespoke per server. → IDF declares `preapproval.requiredFor` with `maxAmount` / `dailySum`.
4. **Destructive hints.** Manual, often forgotten. → IDF: `effect.context.__irr.point === "high"` → `destructiveHint: true` automatic.
5. **Business rules as LLM hint.** Usually not transmitted. → IDF: `intent.conditions` land in tool description as `Preconditions:`.
6. **Domain invariants in descriptions.** Almost never. → IDF computes the relevant invariants per intent (alpha × entity match) and injects them as `May fail on (domain invariants)`. Closes the #1 complaint about hand-rolled MCP servers: *"the server doesn't carry domain semantics — the LLM knows what to call but not why it'll fail."*
---
## How long does authoring an IDF artifact take
Three reference points from the public IDF host runtime:
| Domain | Shape | Time |
|-------------|----------------------------------------------------------------|-------------------------------------|
| `invest` | 14 entities · 61 intents · 5 invariants · ~600 lines | a weekend, hand-written |
| `gravitino` | 253 entities (Apache catalog OpenAPI) · 120 intents | imported in <1h, enriched in 2 days |
| `workflow` | 9 entities · 47 intents · timer queue · cascade rules | a day |
Where the speed comes from (all in `@intent-driven/cli`):
- `idf import postgres` — reads your live schema, generates entity
baseline with FKs and column types as `fieldRole`.
- `idf import openapi` — reads your existing API spec, generates intents
+ parameter shapes + reference fields. *This is how a 253-entity
domain gets bootstrapped.*
- `idf import prisma` — same story for ORM-driven backends.
- `idf enrich` — LLM pass to fill `label`, `fieldRole`, `compositions`,
suggested `roles.agent.preapproval` predicates from your existing
code comments.
The author-once-then-forget loop is the whole point. Once the artifact
exists, you don't regenerate scaffolding on schema change — the runtime
re-reads and serves four readers (UI, voice, agent, document) off the
same file.
---
## Domain prerequisites
The protocol is reliable, but it needs the IDF domain to be authored
correctly. Without these, `tools/list` may return empty,
`tools/call` may return `domain_not_supported`, resources may be empty:
1. **`ontology.roles.agent`** must be declared. No agent role → no tools, no resources.
2. **`role.agent.canExecute`** — list of safe intents. Avoid `__irr:high` without preapproval.
3. **`role.agent.visibleFields`** — array of fields or `"own"` / `"all"` / `"aggregated"` markers.
4. **Server-side effect builder** (`server/schema/effectBuildersRegistry.cjs` in `idf`) must include your domain. Without it `tools/call` returns `domain_not_supported`.
5. **Public catalogs without `ownerField`.** When an entity has `ownerField`, the SDK `filterWorldForRole` filters out rows where `row[ownerField] !== viewer.id`. For public catalogs (e.g. `Task` with `status: "published"`) use `role.scope` with a via-collection or a separate agent-roleable projection (roadmap).
---
## Limitations (1.0)
- `tools` and `resources` only. `prompts` / `completion` — roadmap.
- Bootstrap reads ontology from local FS. SaaS variant (ontology from DB/API) — next.
- Auth: email/password login. PAT / OAuth2 — next.
- Sync only (`POST /exec`). Long-running via MCP tasks API — next.
## Links
- **Landing & demo:** [fold.intent-design.tech](https://fold.intent-design.tech)
- **Quickstart:** [intent-driven-software/fold-runtime-quickstart](https://github.com/intent-driven-software/fold-runtime-quickstart)
- **Host runtime:** [DubovskiyIM/idf](https://github.com/DubovskiyIM/idf)
- **Why a runtime layer:** [paper](https://fold.intent-design.tech/paper/why-runtime-layer/) — ~1800-word essay on the agent-safety class question this answers
- **MCP spec:** [modelcontextprotocol.io](https://modelcontextprotocol.io)
## License
MIT
TDQS
A3.9/5.0
Scored across 6 tools
Disambiguation5/5
Each tool targets a distinct action in the robo-advisor workflow: order execution, signal fetching, anomaly flagging, report generation, rebalance proposal, and risk score recomputation. No two tools have overlapping purposes.
Naming Consistency5/5
All tools follow a consistent verb_noun pattern with the 'agent_' prefix, using imperative verbs like 'execute', 'fetch', 'flag', 'generate', 'propose', and 'recompute' followed by clear nouns.
Tool Count5/5
With 6 tools, the set is well-scoped for a robo-advisor, covering core operations without unnecessary bloat or missing essentials.
Completeness4/5
The tools cover key robo-advisor functions, but missing a tool for viewing portfolio details or managing preapproval settings could cause minor gaps for agents.
Maintenance
ActivityInactive
ResponsivenessNo issues