---
title: "MCP server"
description: "Reference for @oneie/mcp — the stdio server that puts 168 ONE tools inside Claude Code, Claude Desktop, Cursor and Windsurf."
type: reference
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/mcp
---
`@oneie/mcp` is an stdio MCP server that exposes the ONE substrate to any MCP
client. Version 0.6.1 registers **168 tools**. The bin is `oneie-mcp`.

## At a glance

| | |
|---|---|
| Package | `@oneie/mcp` |
| Version | 0.6.1 |
| Bin | `oneie-mcp` → `dist/index.js` |
| Transport | stdio only |
| Tools registered | 168 |
| Depends on | `@oneie/sdk ^0.14.12`, `@oneie/templates ^0.2.0`, `@modelcontextprotocol/sdk ^1.0.0` |
| Ships to npm | `dist/`, `README.md` |
| Telemetry | on by default |

## Telemetry

Telemetry is enabled by default and fires once per tool call, before the
handler runs.

| | |
|---|---|
| Endpoint | `POST https://api.one.ie/api/signal` |
| Rate limit | 100 emits per hour, per server process |
| Fields sent | `sender: toolkit:<sessionId>`, `receiver: toolkit:mcp:<tool>`, tags `["telemetry","mcp",<tool>,"tool-call",<tool>]`, `weight: 1`, `content: { id: <sessionId>, conversationId }` |
| Session id | sha256 of 16 random bytes, first 16 hex chars, generated in-process on first use |
| Not sent | tool arguments, tool results, API key |

Two opt-outs. Either disables it:

```bash
ONEIE_TELEMETRY_DISABLE=1
```

```json
{ "telemetry": false }
```

The JSON above goes in `~/.oneie/config.json`. Any non-empty value of
`ONEIE_TELEMETRY_DISABLE` disables telemetry. The config file is read on every
call; a missing or unparseable file leaves telemetry on.

## Install

```bash
npm install -g @oneie/mcp
```

Or run it without installing:

```bash
npx -y @oneie/mcp
```

Get a key first:

```bash
npm i -g @oneie/cli
oneie auth login
```

## Client configuration

Use the same server key (`oneie`) in every client so tool names read the same
way across them.

| Client | Config location | Source of the block below |
|---|---|---|
| Claude Code | `claude mcp add`, or `.claude/settings.json` | documented in this repo |
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | documented in this repo |
| Cursor | `.cursor/mcp.json`, or `~/.cursor/mcp.json` | Cursor's own documented format; no block exists in this repo |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | Windsurf's own documented format; no block exists in this repo |

All four take the same `mcpServers` object shape.

**Claude Code** — one command, no file editing:

```bash
claude mcp add oneie \
  -e ONEIE_API_KEY=one-<your-key> \
  -e ONEIE_API_URL=https://one.ie \
  -- npx -y @oneie/mcp
```

**Claude Desktop, Cursor, Windsurf** — the file contents:

```json
{
  "mcpServers": {
    "oneie": {
      "command": "npx",
      "args": ["-y", "@oneie/mcp"],
      "env": {
        "ONEIE_API_URL": "https://one.ie",
        "ONEIE_API_KEY": "one-<your-key>"
      }
    }
  }
}
```

**Global install** — same shape, using the bin name instead of `npx`:

```json
{
  "mcpServers": {
    "oneie": {
      "command": "oneie-mcp",
      "env": {
        "ONEIE_API_URL": "https://one.ie",
        "ONEIE_API_KEY": "one-<your-key>"
      }
    }
  }
}
```

## Environment variables

| Variable | Fallback | Default | What |
|---|---|---|---|
| `ONEIE_API_KEY` | `ONE_API_KEY` | none | Bearer token sent on every HTTP call |
| `ONEIE_API_URL` | `ONE_API_URL` | `https://one.ie` | Substrate base URL |
| `ONEIE_TELEMETRY_DISABLE` | — | unset | Any non-empty value disables telemetry |

The default base URL is `https://one.ie` — the app origin, not the gateway.
The gateway serves the substrate unprefixed, so `ONEIE_API_URL=https://api.one.ie`
returns 404 for every tool that makes an HTTP call — that is 165 of the 168; the
three template tools are local and answer either way. The shipped README's
environment table prints `https://api.one.ie` as the default; that table is
wrong. Omit the variable unless you are pointing at a non-production origin.

`readEnv()` runs once, at `createRouter()`. Changing an environment variable
requires restarting the server.

## Auth

Two doors.

| Door | Key required | Path |
|---|---|---|
| Bearer | yes | `Authorization: Bearer <ONEIE_API_KEY>` on every HTTP call |
| `auth_agent` | no | `POST /api/auth/agent`, called with the key deliberately unset |

`auth_agent` is the keyless onboarding call. With no `ONEIE_API_KEY` set, the
server still starts and every tool is listed. Calling `auth_agent` with `{}` —
or `{ wallet: "<sui address>" }` — returns `{ uid, apiKey, wallet, group }`.

It does not route through the `auth:agent` receiver. That receiver is gated
against exactly this call's shape (anonymous, keyless, no `Origin` header,
which a stdio process does not have), so the tool posts to the public
`/api/auth/agent` endpoint instead.

**Restart semantics.** The key `auth_agent` returns does not apply to the
running server. `readEnv()` already ran. Persist the value as `ONEIE_API_KEY`
in the client's config and restart the MCP server before making authenticated
calls.

## Where the 168 tools come from

Three sources, composed in `createOneRouter()`.

| Source | Origin | Contributes | Changes when |
|---|---|---|---|
| Registry-derived | [receivers](/docs/receivers) in [`@oneie/sdk`](/docs/sdk) carrying a `surfaces.mcp` flag | 93 names, 53 with no curated twin | the resolved `@oneie/sdk` changes |
| Curated | hand-written modules in `packages/mcp/src/tools/` | 115 names across 14 modules | `@oneie/mcp` is published |
| Generated `fn_*` | one per entry in `@oneie/sdk/fn-allowlist` | 22 | the allowlist or `FN_MAP` changes |

Registration order:

```
createOneRouter()
  |
  1. mcpToolsFromRegistry()   93 derived names
  |
  2. substrateTools() ... tasksTools()   115 curated
  |
  = 168 registered
     a curated tool of the same name replaces
     the derived one: 40 names are replaced,
     53 derived names survive as the only
     definition
```

The arithmetic:

| Set | Count |
|---|---|
| Curated (14 modules) | 115 |
| Registry-derived names | 93 |
| — replaced by a curated twin | 40 |
| — surviving as the only definition | 53 |
| **Total registered** | **168** |
| Generated `fn_*` (subset of curated) | 22 |
| Total excluding `fn_*` | 146 |

A derived tool is named, described and schema'd from the receiver declaration
itself. The description is the receiver's `summary`, the input schema is
`metaSchema(receiver).request`, and dispatch is `POST /api/ask/<receiver>` with
`{ data: args }`.

The name has two forms. `surfaces: { mcp: true }` munges the receiver — every
character outside `[A-Za-z0-9_-]` becomes `_`, so `world:create-group` ships as
`world_create-group`. `surfaces: { mcp: { name: "..." } }` carries an explicit
name instead, which is how a derived tool keeps the name a since-deleted
hand-written twin already had: `video:create-room` ships as `create_room`, not
`video_create-room`. 22 of the 93 flagged receivers declare a name; 21 of those
names differ from the munge.

Consequence: a receiver flagged `surfaces.mcp` in a newer `@oneie/sdk` becomes
a tool with no `@oneie/mcp` publish. 75 of the 168 tools — 22 `fn_*` plus 53
derived-only — resolve from the SDK, which `package.json` pins as `^0.14.12`.
The tool count tracks the installed SDK, not the MCP release.

Receivers whose zod schema failed JSON-Schema conversion: **0**
(`UNCONVERTIBLE` is empty).

## Curated modules

| Module | Tools |
|---|---|
| substrate | 19 |
| lifecycle | 19 |
| fn | 22 |
| tasks | 14 |
| observability | 8 |
| broadcast | 8 |
| workflow | 7 |
| seo | 6 |
| discovery | 3 |
| social | 3 |
| chat | 2 |
| views | 2 |
| messaging | 1 |
| video | 1 |
| **Total** | **115** |

## The tool surface

`C` = hand-written curated. `D` = registry-derived only.

**Substrate — the verbs and the dimension reads (19, C)**

`signal` · `ask` · `mark` · `warn` · `fade` · `follow` · `select` · `recall` ·
`reveal` · `forget` · `frontier` · `know` · `highways` · `groups` · `actors` ·
`things` · `paths` · `events` · `learning`

**Meta — runtime discovery (4, D)**

`meta_catalog` · `meta_schema` · `meta_types` · `meta_recall`

**Agent lifecycle (19 C, 5 D)**

C: `auth_agent` · `sync_agent` · `publish_agent` · `pull_agent` ·
`unpublish_agent` · `list_agents` · `agent_history` · `rollback_agent` ·
`discover_skill` · `register` · `pay` · `own_me` · `list_skills` ·
`unimport_skill` · `skill_eval` · `tasks_mine` · `tasks_everywhere` ·
`tasks_claim` · `tasks_link`

D: `agent_run` · `skill_run` · `skills_list` · `identity_address` ·
`lifecycle_of`

**Templates and scaffolding — local, no HTTP (3, C)**

`scaffold_agent` · `list_presets` · `get_agent`

**Tasks (14 C, 7 D)**

C: `tasks_list` · `tasks_create` · `tasks_subtask` · `tasks_status` ·
`tasks_priority` · `tasks_tag` · `tasks_notes` · `tasks_rename` ·
`tasks_comment` · `tasks_depend` · `tasks_schedule` · `tasks_approve` ·
`tasks_launch` · `tasks_stake`

D: `tasks_undepend` · `tasks_follow` · `tasks_unfollow` · `tasks_reassign` ·
`tasks_reap` · `tasks_announce` · `tasks_generate`

**Workflow (7 C, 8 D)**

C: `workflow_list` · `workflow_get` · `workflow_apply_diff` · `workflow_run` ·
`workflow_runs` · `workflow_validate` · `workflow_resolve`

D: `workflow_create` · `workflow_update` · `workflow_stop` ·
`workflow_trigger` · `workflow_tql` · `workflow_apply-tql` ·
`workflow_step-stats` · `workflow_apply-diff`

**World — CRUD across the dimensions (8, D)**

`world_create-group` · `world_update-group` · `world_create-actor` ·
`world_update-actor` · `world_list-actors` · `world_create-thing` ·
`world_update-thing` · `world_list-things`

**Group membership (3, D)**

`groups_join` · `groups_leave` · `groups_members`

**Observability (8, C)**

`stats` · `health` · `revenue` · `frontiers_global` · `export_units` ·
`export_highways` · `ingest_event` · `chat_turn`

**Messaging and chat (3, C)**

`message` · `chat_send` · `chat_broadcast`

**Broadcast, newsletter, segments (8, C)**

`broadcast_create` · `broadcast_list` · `broadcast_get` · `broadcast_send` ·
`newsletter_update` · `segment_list` · `segment_get` · `segment_preview`

**Social (3, C)**

`social_list_posts` · `social_create_post` · `social_accounts`

**Video and rooms (1 C, 10 D)**

C: `get_room_status`

D: `create_room` · `delete_room` · `create_session` · `contact_call` ·
`quick_call` · `invite_to_call` · `schedule_webinar` · `start_recording` ·
`start_stream` · `video_summary`

**Wallet (5, D)**

`wallet_get` · `wallet_portfolio` · `wallet_send` · `wallet_invoice` ·
`wallet_transactions`

**Pages (2, D)**

`pages_create` · `pages_list`

**Views (2, C)**

`create_view` · `list_views`

**SEO (6 C, 1 D)**

C: `seo_backlinks` · `seo_ai_visibility` · `seo_research_keywords` ·
`seo_serp` · `seo_keyword_metrics` · `seo_gsc`

D: `seo_keyword-metrics`

## Generated fn_* tools

22 tools, one per entry in `@oneie/sdk/fn-allowlist`. `FN_MAP` holds 270 TypeDB
functions; the allowlist selects which 22 are exposed as tools.

`fn_actionable_hypotheses` · `fn_active_frontiers` · `fn_active_objectives` ·
`fn_actor_classification` · `fn_actors_by_kind` · `fn_actors_by_tag` ·
`fn_actors_for` · `fn_attractive_tasks` · `fn_blocked_tasks` ·
`fn_cheapest_provider` · `fn_covered-unproven` · `fn_deadlocked` ·
`fn_exploratory_tasks` · `fn_highways` · `fn_incomplete` · `fn_optimal_route` ·
`fn_promising_frontiers` · `fn_ready-tasks` · `fn_suggest_route` ·
`fn_total_contribution` · `fn_uncovered` · `fn_unrealised`

| | |
|---|---|
| Dispatch | `POST /api/ask/fn%3Arun` with `{ data: { fn, args } }` |
| Argument shaping | every argument key is `$`-prefixed before the call |
| Schema | all parameters typed `string`, all required, `additionalProperties: false` |
| Description | carries the `FN_MAP` content hash, e.g. `FN_MAP va8ccc426…` |

A function in `FN_MAP` that is not allowlisted has no tool, and `ask({ receiver:
"fn:run", data: { fn, args } })` does not reach it either. The `fn:run` resolver
checks the same allowlist server-side, before it looks the function up in
`FN_MAP`, and answers `{ ok: false, rows: [], error: "fn '<name>' is not in the
allowlist" }`. The allowlist bounds dispatch, not only the tool list.

## conversation_id

`serve()` injects a `conversation_id` property into **every** tool's
`inputSchema` at `ListTools` time. It is not declared by any tool module and it
is not in `required`.

```json
{
  "conversation_id": {
    "type": "string",
    "description": "Conversation trace ID — mint on first call, echo on every subsequent call to link the chain."
  }
}
```

| Step | Behaviour |
|---|---|
| `ListTools` | the property is added to all 168 schemas |
| `CallTool`, field present | value is read for telemetry, then stripped |
| `CallTool`, field absent | telemetry falls back to the process session id |
| Handler | never sees the field |

The contract is mint-on-first-call, echo-thereafter. The value correlates a
call chain in telemetry. It reaches no receiver.

## Discovery

There is no `__list` tool. The shipped README instructs readers to call one;
the router does not register it.

| Step | Call | Returns |
|---|---|---|
| 1 | `ListTools` — the client sends this on connect | all 168 names, descriptions, input schemas |
| 2 | `meta_catalog`, or `ask({ receiver: "meta:catalog" })` | the typed receiver surface, role-scoped, with cost, reversibility and settlement |
| 3 | `meta_schema`, or `ask({ receiver: "meta:schema", data: { receiver } })` | one receiver's payload shape |
| 4 | `ask({ receiver, data })` | the result |

Every [receiver](/docs/receivers) in the catalog is reachable through `signal`
and `ask` whether a dedicated tool exists for it or not. The `surfaces.mcp`
flag controls only whether a receiver is listed as its own tool. The `signal`
and `ask` descriptions both end with a generated recipe menu and a pointer to
`meta:catalog`.

## Results and errors

| Outcome | MCP response |
|---|---|
| Success | `content: [{ type: "text", text: JSON.stringify(result, null, 2) }]` |
| Failure | `isError: true`, `content: [{ type: "text", text: JSON.stringify({ error }) }]` |
| HTTP 204 | the success row, with `apiCall` returning `null` — `text` is `"null"` |

A non-2xx HTTP response throws with the response body embedded in the message,
truncated to 1000 characters. The substrate returns structured 400 bodies —
`{ error, field, expected, got, hint, example }` — so the failing field and an
example payload reach the caller instead of a bare status code.

## Programmatic use

```ts
import { createOneRouter, serve, MCP_VERSION } from "@oneie/mcp";

const router = createOneRouter();
await serve(router, { name: "oneie", version: MCP_VERSION });
```

Exported from the package root: `createRouter`, `createOneRouter`, `serve`,
`apiCall`, `readEnv`, `MCP_VERSION`, `MCP_TOOLS`, `toolManifestDrift`,
`mcpToolsFromRegistry`, `derivedToolNames`, `toolNameFor`, `autoToolName`,
`UNCONVERTIBLE`, the types `McpTool` and `McpRouter`, and 13 of the 14 tool
factories — `substrateTools`, `discoveryTools`, `lifecycleTools`,
`observabilityTools`, `videoTools`, `broadcastTools`, `messagingTools`,
`chatTools`, `fnTools`, `seoTools`, `viewsTools`, `workflowTools`,
`tasksTools`.

`socialTools` is the exception. It is exported from `src/tools/social.ts` but
not re-exported from the package root, so it cannot be imported from
`@oneie/mcp`.

Count the live surface:

```bash
node -e "import('@oneie/mcp').then(m=>console.log([...m.createOneRouter().tools.keys()].length))"
```

## Known limits

**Two shadow-duplicate tool names ship.** Same receiver, two live names, two
different descriptions. Use the underscore variant.

| Use this | Not this | Receiver |
|---|---|---|
| `workflow_apply_diff` | `workflow_apply-diff` | `workflow:apply-diff` |
| `seo_keyword_metrics` | `seo_keyword-metrics` | `seo:keyword-metrics` |

`workflow_apply_diff` also shapes its arguments — it sends
`simulate: commit !== true`, so it simulates by default. The hyphen variant
forwards arguments unchanged.

**No `__list` tool.** The README instructs readers to call one. Use `ListTools`
and `meta_catalog` instead.

**The README's environment table is wrong.** It prints `https://api.one.ie` as
the `ONEIE_API_URL` default. The code defaults to `https://one.ie`, and
`api.one.ie` returns 404 for every tool that makes an HTTP call.

**stdio only.** There is no HTTP or remote MCP endpoint. The
`api.one.ie/mcp/:tool` route was designed and never built.

**`auth_agent` needs a restart.** The key it returns does not apply to the
running server.

**An empty result is not proof of an empty board.** An authorised call against
a workspace you do not belong to returns `ok: true` with zero rows,
indistinguishable from no work queued. Treat zero rows as an unproven
connection until a call returns rows.

**Write task edges serially.** Batching `tasks_depend` or `tasks_tag` in one
parallel block produces spurious `not_found` and `write_failed` responses; the
same calls succeed on serial retry with identical arguments. The errors read
like permission failures.

**The tool count tracks the SDK.** 75 of the 168 tools resolve from
`@oneie/sdk` (`^0.14.12`). A different resolved SDK gives a different count at
the same `@oneie/mcp` version.

**`check:tools` does not check documentation.** `bun run check:tools` compares
the hand-maintained `MCP_TOOLS` manifest against the live router and reads no
README. It exempts `fn_*` names and derived-only names by design. Measured
drift at 0.6.1: 0 missing, 0 extra.