# Real Signal MCP — tool reference *Real Signal MCP · reference · 2026-06-07* This is the authoritative reference for every tool exposed by `https://real-signal.ai/api/mcp`. Schemas, examples, and error cases for 11 tools — the 10 from versions 0.1.0–0.3.0 plus `score_legitimacy` shipping in 0.4.0. The live tool count is in [status](/mcp/STATUS.md); schema changes are tracked in the [changelog](/mcp/CHANGELOG.md). ## Common envelope Every `tools/call` response returns a JSON-RPC 2.0 envelope with two payload halves and one attribution block: - **`content[]`** — human-readable text the AI assistant can quote verbatim. Voice-locked: lowercase, observational, ≤18 words per emit-style line. - **`structuredContent`** — typed object the AI assistant can reason over. Older clients without structured-content support get only `content[]`. - **`_meta`** — attribution envelope. Source, license, computed_at, tool name. Citation is required by license; see [CC BY-NC-ND 4.0](https://real-signal.ai/LICENSE-CONTENT.md). A typical `_meta` block: ```json { "source": "real-signal.ai", "source_url": "https://real-signal.ai", "license": "CC BY-NC-ND 4.0", "license_url": "https://real-signal.ai/LICENSE-CONTENT.md", "attribution_required": true, "tool": "get_pocket_moment", "computed_at": "2026-06-06T08:18:42.000Z" } ``` ## Common error cases Every tool returns `{ isError: true, content: [{ type: "text", text: "error: " }] }` on failure rather than throwing. Errors never propagate to the JSON-RPC layer for tool-level failures — only protocol violations do. | Error message | Trigger | |---|---| | `error: supabase not configured` | The server's `SUPABASE_URL` / `SUPABASE_ANON_KEY` env vars are missing. | | `error: is required` | The named field is missing or empty in `arguments`. | | `error: unknown tool: ` | A `tools/call` request named a tool not in the registry. | | `error: query too long (max 80 chars)` | `lookup_pocket_by_name` query exceeds the 80-character cap. | JSON-RPC protocol errors (`-32600`, `-32601`, `-32602`, `-32700`, `-32603`) are returned only for malformed envelopes, unknown methods, or parse failures — not for tool-level errors. --- ## `list_pockets` Return the list of active neighbourhood pockets the agent observes. A pocket is a small physical neighbourhood (Cluny Court, Holland Village). Reads from `pocket_moment_quality_latest` — the canonical "pockets the agent is actively observing." **Input schema:** no arguments. | name | type | required | description | |---|---|---|---| | *(none)* | — | — | — | **Output (`structuredContent`):** ```json { "pockets": ["cluny", "holland-village", "tiong-bahru"] } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_pockets", "arguments": {} } } ``` **Example response:** ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "observed pockets: cluny, holland-village, tiong-bahru." }], "structuredContent": { "pockets": ["cluny", "holland-village", "tiong-bahru"] }, "_meta": { "source": "real-signal.ai", "tool": "list_pockets", "computed_at": "2026-06-06T08:18:42.000Z" } } } ``` **Error cases:** `supabase not configured`. --- ## `get_pocket_moment` Return the agent's current read of a pocket: atmosphere primary state, calm probability, signal saturation, movement friction, fragility, window half-life. Use this when an AI assistant needs to know *what is the calm window in this neighbourhood right now* before deciding whether to surface anything. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier (e.g. `"cluny"`) | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "primary_state": "calm-productive", "calm_probability": 0.71, "signal_saturation": 0.18, "movement_friction": 0.22, "fragility": 0.31, "half_life_minutes": 42, "expires_at": "2026-06-06T08:30:00Z", "should_stay_silent": false, "silence_reasons": [] } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_pocket_moment", "arguments": { "pocket_id": "cluny" } } } ``` **Example response:** ```json { "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "pocket cluny · atmosphere: calm-productive · calm: 0.71 · saturation: 0.18 · movement friction: 0.22 · fragility: 0.31 · window expires: 2026-06-06T08:30:00Z" }], "structuredContent": { "pocket_id": "cluny", "primary_state": "calm-productive", "calm_probability": 0.71, "signal_saturation": 0.18, "movement_friction": 0.22, "fragility": 0.31, "half_life_minutes": 42, "expires_at": "2026-06-06T08:30:00Z", "should_stay_silent": false, "silence_reasons": [] }, "_meta": { "source": "real-signal.ai", "tool": "get_pocket_moment", "computed_at": "2026-06-06T08:18:42.000Z" } } } ``` **Error cases:** `pocket_id is required`; `no moment composable for — substrate may be thin.` (returned as content, not isError, when the pocket has no recent atmosphere readings). --- ## `get_observed_outlet` Return the public shadow-profile data for an outlet: outlet metadata, observed patterns drawn from substrate (atmosphere, DNA, rain sensitivity), and aggregate-only decision-support lines drawn from the pocket. Aggregate basis n ≥ 5 enforced; nothing names competitors. **Input schema:** | name | type | required | description | |---|---|---|---| | `outlet_id` | string | yes | outlet UUID | **Output (`structuredContent`):** ```json { "outlet": { "id": "outlet-uuid", "name": "Plain Vanilla", "pocket_id": "cluny", "unit": "#01-15", "opens_at": "08:00", "closes_at": "20:00", "category": "bakery" }, "patterns": [ "calmest hour: 14:00–15:00 (n=22 atmosphere readings)" ], "decision_support": [ "the pocket's recovery ledger shows ~$321 over 30d across 5+ outlets" ] } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_observed_outlet", "arguments": { "outlet_id": "outlet-uuid" } } } ``` **Error cases:** `outlet_id is required`; `outlet not found or inactive.` (returned as content when the outlet is unknown or `active=false`). --- ## `get_pocket_sustainability` Return the pocket-level sustainability ledger summary — physical impact (units kept out of waste, reclaimed hours, trips matched, attention saved) and SGD dollar recovery, rendered in the merchant audience voice. Window is configurable (default 30 days). **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier | | `window_days` | integer | no | 1–365, default 30 | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "window": "30d", "waste_avoided_units": 340, "idle_reclaimed_hours": 14.5, "purposeful_trips": 88, "attention_saved_count": 412, "dollar_value_sgd": 1284.0 } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "get_pocket_sustainability", "arguments": { "pocket_id": "cluny", "window_days": 30 } } } ``` **Error cases:** `pocket_id is required`; `no sustainability data for over the last d.` (returned as content when the ledger is empty for the window). --- ## `get_pocket_moment_quality` Return the Moment Quality Score (MQS) for a pocket — the central *is this moment worth breaking silence?* scalar. Composes five orthogonal factors (usefulness, urgency, calmness, merchant_fit, user_need) multiplicatively into a [0..1] score, plus a band (silent / weak / forming / high_resonance / peak / decaying). Reads the latest cron-written reading from `pocket_moment_quality_latest`. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "computed_at": "2026-06-06T08:05:00Z", "mqs": 0.62, "band": "high_resonance", "factors": { "usefulness": 0.78, "urgency": 0.55, "calmness": 0.81, "merchant_fit": 0.70, "user_need": 0.84 }, "previous_mqs": 0.51, "reasons": ["calmness leading at 0.81", "user_need rising from 0.62"], "source": "cache" } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "get_pocket_moment_quality", "arguments": { "pocket_id": "cluny" } } } ``` **Error cases:** `pocket_id is required`; `no MQS data for yet — substrate may be thin.` (returned as content when the moment-quality-loop cron has not yet written a row for the pocket). --- ## `lookup_pocket_by_name` Resolve a free-text place name ("Holland Village", "cluny court", "Bukit Timah") to a canonical pocket_id. Pure fuzzy match over the pockets registry — no database round-trip. Returns up to 3 best matches with confidence ≥ 0.2. Use this BEFORE any pocket-scoped tool when the user names a place rather than a slug. **Input schema:** | name | type | required | description | |---|---|---|---| | `query` | string | yes | 1–80 chars, free-text place name | **Output (`structuredContent`):** ```json { "query": "Holland Village", "matches": [ { "pocket_id": "holland-village", "display_name": "Holland Village", "lat": 1.3115, "lng": 103.7956, "region": "queenstown", "confidence": 1.0 } ] } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "lookup_pocket_by_name", "arguments": { "query": "Holland Village" } } } ``` **Error cases:** `query is required`; `query too long (max 80 chars)`; `no pocket matched '' — try 'holland village'.` (returned as content when no match exceeds confidence 0.2). --- ## `get_pocket_atmosphere` Return the 15-minute atmosphere stream for a pocket — latest reading (stress, calm, social_energy, productive, primary_state, anomaly_flag) plus a trend label (`rising_calm`, `rising_stress`, `stable`, `mixed`) computed across the window. Use this when you need trajectory, not just the now. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier | | `hours` | number | no | 1–24, default 4 | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "window_hours": 4, "latest": { "stress": 0.18, "calm": 0.74, "social_energy": 0.42, "productive": 0.68, "primary_state": "calm-productive", "anomaly_flag": false, "captured_at": "2026-06-06T08:15:00Z" }, "trend": { "direction": "rising_calm", "magnitude": 0.22 }, "reading_count": 16 } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "get_pocket_atmosphere", "arguments": { "pocket_id": "cluny", "hours": 6 } } } ``` **Error cases:** `pocket_id is required`; `pocket has no recent atmosphere readings — atmosphere-loop cron may be stale.` (returned as content with `reading_count: 0` when the window contains no rows). --- ## `get_pocket_predictions` *(ships in 0.3.0)* Return the predictions ledger — sealed forecasts, their reveal times, and accuracy scores once revealed. The trust-infrastructure read; lets an AI assistant audit Real Signal's track record before quoting an observation. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | no | filter to one pocket | | `generator` | string | no | filter to one generator key (e.g. `pocket-projection-60m`) | | `since` | string | no | `'1h'` / `'1d'` / `'7d'` / `'30d'` / `'all'`, default `'7d'` | | `revealed_only` | boolean | no | default `false` | | `limit` | integer | no | 1–500, default 50 | **Output (`structuredContent`):** ```json { "count": 41, "window": "30d", "accuracy_by_generator": [ { "generator": "pocket-projection-60m", "n": 41, "accuracy": 0.73 } ], "entries": [ { "id": "ledger-row-uuid", "pocket_id": "cluny", "generator": "pocket-projection-60m", "prediction_key": "cluny:2026-06-06:14:00", "sealed_at": "2026-06-06T05:00:00Z", "reveal_at": "2026-06-06T06:00:00Z", "revealed": true, "prediction_payload": { "calm_probability": 0.71 }, "actual_observation": { "calm_probability": 0.68 }, "accuracy_score": 0.94 } ] } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": { "name": "get_pocket_predictions", "arguments": { "since": "30d", "revealed_only": true } } } ``` **Error cases:** `since must be one of 1h, 1d, 7d, 30d, all`; `limit must be 1–500`. --- ## `get_pocket_silence` *(ships in 0.3.0)* Return why the agent is currently silent in a pocket — the voice-locked active-silence justification, factor breakdown, predictive outlook, and "next possible window" anticipation. Doctrine-aligned: explains both speech and silence. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | no | when absent, returns network-level overview | **Output (`structuredContent`):** ```json { "pocket_id": "holland-village", "signal_status_code": "WITHHOLD_INTENTIONAL", "justification": "noise saturation 0.62, above the 0.40 ceiling", "outlook": ["window may form by ~17:00 SGT"], "next_possible_window": "2026-06-06T09:00:00Z", "substrate_vitals": { "pockets_watched": 6, "readings_per_hour": 24, "open_predictions": 4 } } ``` Signal status codes: `STABLE_CALM`, `WITHHOLD_INTENTIONAL`, `FORMING_WINDOW`, `ACTIVE_INTERVENTION`, `DECAY_WINDOW`. **Example request:** ```json { "jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": { "name": "get_pocket_silence", "arguments": { "pocket_id": "holland-village" } } } ``` **Error cases:** `no silence data for ` (returned as content when the pocket has no readings). --- ## `get_intent_substrate` *(ships in 0.3.0)* Return the data behind an `/intent/:pocket/:slug` page — *I want X in Y* treated as a first-class substrate query. There are 522 valid (pocket × intent) combinations. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier | | `slug` | string | yes | intent slug (e.g. `calm-work`, `rain-shelter`, `quiet-now`) | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "slug": "calm-work", "intent_description": "a quiet seat with power for focused work", "matched_outlets": [ { "id": "outlet-uuid", "name": "Plain Vanilla", "alignment": 0.81, "reasons": ["productive 0.68", "low stress 0.18"] } ], "current_moment_alignment": 0.74, "narrative_line": "three calm-work options at Cluny right now; alignment 0.74." } ``` **Example request:** ```json { "jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": { "name": "get_intent_substrate", "arguments": { "pocket_id": "cluny", "slug": "calm-work" } } } ``` **Error cases:** `pocket_id is required`; `slug is required`; `unknown intent slug: ` (returned as content when the slug is not in the intents registry). --- ## `score_legitimacy` *(ships in 0.4.0)* Score whether a candidate action against a Real Signal pocket would be legitimate, on a 0-100 scale. Composes Moment composite, MQS, user-fatigue, signal-saturation, and merchant-pressure into a single restraint score with reasoning. **The query every AI assistant should ask before interrupting.** Also available as a direct HTTP endpoint at `POST /api/legitimacy` — same args, same response shape, same `_meta` attribution envelope. **Input schema:** | name | type | required | description | |---|---|---|---| | `pocket_id` | string | yes | pocket identifier (e.g. `"cluny"`) | | `candidate_action` | string | yes | short action name being considered, e.g. `"surface_deal"` | | `user_context` | object | no | `{ user_id?, fatigue_score?, last_dismissal_at? }` — explicit values take precedence over computed lookups | | `merchant_context` | object | no | `{ id?, idle_pressure? }` — explicit values take precedence over defaults | | `channel` | string | no | optional channel hint (`"push"`, `"email"`, `"sms"`) | **Output (`structuredContent`):** ```json { "pocket_id": "cluny", "candidate_action": "surface_deal", "channel": null, "legitimacy_score": 47, "band": "borderline", "recommendation": "borderline_remain_silent", "reasoning": [ "dominant factor signal saturation 0.61.", "borderline · agent defaults to silence at band borderline." ], "factors": { "environmental_alignment": 0.72, "fatigue": 0.30, "saturation": 0.61, "merchant_pressure": 0.55, "expected_value": 0.42 }, "gates": { "resonance": { "passed": true, "score": 0.72 }, "moment_silence": { "passed": true, "score": 1 }, "why_this_place": { "passed": true, "score": 1 }, "why_now": { "passed": true, "score": 0.55 }, "why_this_person": { "passed": true, "score": 0.70 }, "why_worth_attention": { "passed": false, "score": 0.18 }, "why_low_effort": { "passed": true, "score": 1 } }, "vindication_window_end": "2026-06-07T07:42:00.000Z", "computed_at": "2026-06-07T06:42:00.000Z" } ``` **Bands:** `legitimate` (score ≥ 70) · `borderline` (40 ≤ score < 70) · `remain_silent` (score < 40). **Recommendations:** `emit` (band=legitimate, no hard override) · `borderline_remain_silent` (band=borderline; doctrine default is silence) · `remain_silent` (band=remain_silent or any hard override fired). **Hard overrides** — any of these force `remain_silent` regardless of score: - `Moment.should_stay_silent = true` (saturation or attention floor) - `fatigue > 0.85` (user over-talked-at) - `saturation > 0.80` (pocket over-talked-at) **Example request:** ```json { "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": { "name": "score_legitimacy", "arguments": { "pocket_id": "cluny", "candidate_action": "surface_deal", "user_context": { "fatigue_score": 0.30 } } } } ``` **Example response:** ```json { "jsonrpc": "2.0", "id": 11, "result": { "content": [{ "type": "text", "text": "pocket cluny · candidate surface_deal · score 47 (borderline) · recommendation borderline_remain_silent · dominant factor signal saturation 0.61" }], "structuredContent": { "...": "(see Output above)" }, "_meta": { "source": "real-signal.ai", "tool": "score_legitimacy", "computed_at": "2026-06-07T06:42:00.000Z" } } } ``` **Error cases:** `pocket_id is required`; `candidate_action is required`; `supabase not configured`. --- ## Discovery A GET request to `/api/mcp` returns the discovery manifest — server info, protocol version, transport, and the full tools list. The well-known location is `/.well-known/mcp.json`. Both are public and unauthenticated. ## Rate limiting POST traffic to `/api/mcp` is rate-limited per IP — currently 30 requests per minute, shared with the narration limiter. The response on throttle is an HTTP 429 with a JSON body. There is no `Retry-After` header today; this is on the roadmap and will arrive in 1.0.0. ## License All tool responses are licensed under [CC BY-NC-ND 4.0](https://real-signal.ai/LICENSE-CONTENT.md). Citation is required by `_meta.attribution_required: true`. Commercial reuse and derivative works require permission from `hello@real-signal.ai`.