diff --git a/README.md b/README.md
index 590b1cf6..e5d96dd6 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Commonality is a system for decentralized crowdfunding of public goods: people c
- [High-level project status](./workflow/project-status.md) AKA what milestone are we currently heading for: never deployed to mainnet yet, just did first testnet deployment, see also [MVP](./specs/product/mvp.md)
- Product boundaries between the eight sites: [product UI domains](./specs/product/ui-domains.md).
- Technical domain composition and live route ownership: [technical UI domains](./specs/tech/ui-domains.md) and the actual domain manifests under [`ui/src/domains/`](./ui/src/domains/).
- - **CauseStarter** (SPA in [`ui/src/causestarter/`](./ui/src/causestarter/), `VITE_DOMAIN=causestarter`; glue/backlog [`causestarter/`](./causestarter/), [`causestarter/TODO.md`](./causestarter/TODO.md)): founder-first core domain (eventual primary entry; Tally / LazyGiving / etc. de-emphasized as tools). Included in `./scripts/services.sh --start` and `./scripts/deploy-causestarter.sh`. Local start currently publishes **only** the CauseStarter IPFS bundle (`LOCAL_UI_DOMAINS`; restore all UIs with `LOCAL_UI_DOMAINS=all` — see [local development](./workflow/local-development.md)). LLM helpers: [`cause-assist/`](./cause-assist/).
+ - **CauseStarter** (SPA in [`ui/src/causestarter/`](./ui/src/causestarter/), `VITE_DOMAIN=causestarter`; glue/backlog [`causestarter/`](./causestarter/), [`causestarter/TODO.md`](./causestarter/TODO.md)): founder-first core domain (eventual primary entry; Tally / LazyGiving / etc. de-emphasized as tools). Included in `./scripts/services.sh --start` and `./scripts/deploy-causestarter.sh`. Local start currently publishes **only** the CauseStarter IPFS bundle (`LOCAL_UI_DOMAINS`; restore all UIs with `LOCAL_UI_DOMAINS=all` — see [local development](./workflow/local-development.md)). LLM helpers: [`cause-assist/`](./cause-assist/). Protocol MCP: [`mcp/`](./mcp/). Agent job map: [`docs/end-user/causestarter/for-llms.md`](./docs/end-user/causestarter/for-llms.md).
- [Glossary](./specs/glossary.md) — the project's ubiquitous language. Read before naming anything; it also lists the known terminology drift.
- [Architecture Decision Records](./specs/decisions/README.md) — immutable log of *why* consequential decisions were made (and what was rejected). Grep before reversing something that looks wrong.
- [Role-based guidance](./workflow/roles/README.md) on what docs to read:
diff --git a/causestarter/README.md b/causestarter/README.md
index 24eb1a21..d4e23632 100644
--- a/causestarter/README.md
+++ b/causestarter/README.md
@@ -219,7 +219,9 @@ See [`cause-assist/README.md`](../cause-assist/README.md). Bridge-cluster wordin
catalog. Do not restore Start → Grow → Deliver or “build a Movement.”
- **In-app docs** (`/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`,
and `commonality/` via `endUserDocsPlugin`. Keep markdown links relative so
- they resolve in that viewer.
+ they resolve in that viewer. Agent/protocol index: `/docs/for-llms`. Generated
+ SDK/contract docs: `/api-docs/sdk/` and `/api-docs/contracts/` (Vite serves
+ them in dev; `npm run build:docs` regenerates). Stdio MCP: `mcp/`.
- **CauseStarter is a lens, not a directory** ([ADR 0008](../specs/decisions/0008-operated-surfaces-are-lenses.md)).
It authors no discovery: no search, browse, ranking, featuring, or leaderboards.
A cause is reached at `/cause/:causeId` through a link its organizer circulates.
@@ -292,6 +294,8 @@ startup_timeout_sec = 90
Register once with: `grok mcp add playwright -- npx -y @playwright/mcp@latest`
Then **restart Grok** so MCP tools load.
+Protocol reads (SDK / IPFS / attesters), not the browser: [`mcp/README.md`](../mcp/README.md) (`npm run mcp`).
+
3. Chromium for Playwright tests (repo root):
`npx playwright install chromium`
diff --git a/docs/end-user/causestarter/for-llms.md b/docs/end-user/causestarter/for-llms.md
new file mode 100644
index 00000000..24d8bca1
--- /dev/null
+++ b/docs/end-user/causestarter/for-llms.md
@@ -0,0 +1,74 @@
+# CauseStarter API map (for LLMs)
+
+Task-oriented index for agents. Signatures live in generated TypeDoc; this page only maps **jobs → imports / HTTP**. Do not treat CauseStarter screens as an API — they will change.
+
+Human briefing: [CauseStarter](./index.md). Concept orientation: [tldr-for-llms.md](../tldr-for-llms.md).
+
+## Generated references (regenerate, don’t hand-write)
+
+On a deployed site these are static files (not React routes):
+
+- **SDK TypeDoc:** [/api-docs/sdk/](/api-docs/sdk/)
+- **Solidity contract docs:** [/api-docs/contracts/index.md](/api-docs/contracts/index.md)
+
+Regenerate with `npm run build:docs` at the repo root (`typedoc` on `sdk/`, solc docs on `hardhat/`). Local Vite (`causestarter:dev` on :5174) serves the same trees at those paths when the generated folders exist.
+
+There is **no CauseStarter REST API**. The UI is a lens over the SDK plus a few helper HTTP services.
+
+## What is already in the SDK (including IPFS)
+
+Import subpaths of `@commonality/sdk` (no flat barrel). Construct machinery with `createSDKMachinery` from `@commonality/sdk/machinery`. Node env helpers: `@commonality/sdk/node` (`createIPFSConfigInNodeJSFromTheUsualEnvVars`).
+
+| Job | Import | Functions |
+| --- | --- | --- |
+| Fetch / upload IPFS | `@commonality/sdk/utils` | `fetchFromIPFS`, `uploadToIPFS`, `uploadBlobToIPFS`. Upload needs `ipfsConfig.apiUrl` (Kubo `/api/v0/add`). Fetch needs `gatewayUrl`. |
+| Publish statement bytes on-chain | `@commonality/sdk/published-data` | `publishData`, resolvers. Content-addressed documents also go through displayable-documents. |
+| Create a signable statement and sign it | `@commonality/sdk/conceptspace` | `createAndSignStatement` (upload + PublishedData + `believeStatement`). Also `believeStatement` / `disbelieveStatement` / `clearOpinion`. |
+| Read a statement | `@commonality/sdk/conceptspace` | `getStatement`, `getStatementWithContent`, `getUserBelief`, believer-set helpers. |
+| Implication graph | `@commonality/sdk/conceptspace` | `getImplicationsFrom`, `getImplicationsTo`, `getImplication`. On-chain attest is `attestImplication` (usually an attester service, not a donor). |
+| Cause board as a named ref | `@commonality/sdk/mutable-refs` | `getUserRef`, `getUserRefs`. Organizers publish a roster document and point a named ref at it. |
+| Projects / pledges | `@commonality/sdk/lazy-giving`, `@commonality/sdk/fundingportals`, `@commonality/sdk/delegation` | `getProject`; alignment/success vouches; notes and standing pledges. |
+| Indexer | `@commonality/sdk/utils` event-cache client; `@commonality/sdk/indexer-sync` | Raw events only. Fold in the SDK. `GET {EVENT_CACHE_URL}/status`. |
+
+Local defaults after a stack start: `IPFS_API` → localhost:5001, `IPFS_GATEWAY` → localhost:8080/ipfs, `EVENT_CACHE_URL` → localhost:42069, plus the usual `*_CONTRACT_ADDRESS` env vars.
+
+## HTTP services that are *not* the SDK
+
+These are optional operators. Anyone can run their own. Users trust *identities* (attester / nudger addresses), not “the platform.”
+
+| Service | Typical local URL | What an agent calls | Notes |
+| --- | --- | --- | --- |
+| **Cause assist** | localhost:3002 | `POST /atomize`, `/sharpen-plank`, `/draft-anchor`, `/check-implications`, `/safety-check`, `/check-coherence`, bridge-cluster draft verbs | Wording help only. **No chain writes.** See `cause-assist/README.md`. |
+| **Implication attester** | localhost:3006/implication-attester (compose path) | `POST /evaluate-implication`, `POST /evaluate-implications-batch`, `GET /health`, `/quote`, `/attester-status` | x402 payment on evaluate. Positive evaluations publish on-chain. |
+| **Content attester** | content-attester package | `POST /evaluate-content` (batch too) | Same attester-core HTTP shape; different prompt/domain. |
+| **Finders** | implication-finder, content-finder | Usually **no public “please find” API** | Poll event cache, POST candidates to an attester. |
+| **Nudgers** | implication-graph-nudger, explorer-curator, bridge-creator | `GET /.well-known/nudger.json`, `POST /suggest` (curator), `POST /propose-bridge` (bridge-creator) | Suggestions are signed publications; users opt in. |
+| **Platform API** | localhost:3001 | onramp, sponsored gas, policy content | Ops for the operated UIs, not the protocol. |
+| **Coherence badge worker** | worker package | operator-only chain write | CauseStarter site operator mints badges; cause-assist HTTP must not hold that key. |
+
+Attester-core shared routes: `GET /health`, `GET /quote`, plus a per-service status path.
+
+## MCP in this repo
+
+Package `@commonality/mcp` (`mcp/`). Stdio server wrapping the SDK reads (and optional writes) plus the HTTP helpers above.
+
+```bash
+npm run mcp --workspace=@commonality/mcp
+```
+
+Grok:
+
+```toml
+[mcp_servers.commonality]
+command = "npm"
+args = ["run", "mcp", "--workspace=@commonality/mcp"]
+```
+
+Writes (`upload_ipfs`, `believe_statement`) stay off unless `COMMONALITY_MCP_WRITES=1` and a key is set. Implication evaluate may still return HTTP 402 (x402).
+
+## Rules of thumb
+
+- Prefer SDK queries over scraping CauseStarter.
+- Prefer `getStatementWithContent` over raw gateway fetches when you want displayable JSON.
+- Do not invent a cause directory; you need an owner address + slug/ref name, or a statement CID.
+- Do not treat an attester HTTP 200 as “the protocol agrees.” It is one identity’s attestation, filtered by the viewer’s trust graph.
diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md
index 72ec1d7f..105b8551 100644
--- a/docs/end-user/causestarter/index.md
+++ b/docs/end-user/causestarter/index.md
@@ -52,6 +52,10 @@ Most products with that pitch ask you to join something, trust a black box, or b
You do not need the longer [vision and strategy](../commonality/vision-and-strategy/README.md) notes to use a cause board. They are there if you want the civilizational argument.
+## For assistants and developers
+
+The UI is a lens, not the protocol. **[CauseStarter API map (for LLMs)](./for-llms.md)** maps jobs (sign, publish, pledge, vouch, IPFS, attesters) onto SDK imports and helper HTTP services. Generated **[SDK API docs](/api-docs/sdk/)** and **[contract docs](/api-docs/contracts/index.md)** are the signature reference (`npm run build:docs`). A stdio MCP server lives in the `mcp/` package.
+
## See it as a story
- [A neighborhood throws a block party](../shared/use-case-walkthroughs/block-party.md)
diff --git a/docs/end-user/shared/for-crypto-natives.md b/docs/end-user/shared/for-crypto-natives.md
index 8827d3cd..608d3cad 100644
--- a/docs/end-user/shared/for-crypto-natives.md
+++ b/docs/end-user/shared/for-crypto-natives.md
@@ -39,8 +39,9 @@ The SDK also fetches IPFS content directly from a gateway, so statement content
The SDK and smart contract documentation is auto-generated:
-- **SDK API docs** ([sdk/docs/api/](https://github.com/AdamSpitz/commonality/tree/master/sdk/docs/api)) — TypeScript SDK reference, auto-generated via typedoc from the SDK source.
-- **Contract docs** ([hardhat/docs/](https://github.com/AdamSpitz/commonality/tree/master/hardhat/docs)) — Solidity contract reference, auto-generated via solc doc.
+- **SDK API docs** ([sdk/docs/api/](https://github.com/AdamSpitz/commonality/tree/master/sdk/docs/api) or [/api-docs/sdk/](/api-docs/sdk/) on a deployed UI) — TypeScript SDK reference, auto-generated via typedoc from the SDK source. IPFS fetch/upload is in `@commonality/sdk/utils` (`fetchFromIPFS`, `uploadToIPFS`).
+- **Contract docs** ([hardhat/docs/](https://github.com/AdamSpitz/commonality/tree/master/hardhat/docs) or [/api-docs/contracts/index.md](/api-docs/contracts/index.md)) — Solidity contract reference, auto-generated via solc doc.
+- **CauseStarter job map for agents** ([causestarter/for-llms.md](../causestarter/for-llms.md)) — which SDK subpath or HTTP service (attesters, cause-assist, nudgers) to use; not a second TypeDoc.
Run `npm run build:docs` to regenerate these.
diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md
index 8065feec..a9300fa9 100644
--- a/docs/end-user/tldr-for-llms.md
+++ b/docs/end-user/tldr-for-llms.md
@@ -7,6 +7,14 @@ Each entry follows the same shape:
- **When a user encounters it** — UI surfaces or moments in the user's flow where this concept becomes load-bearing.
- **What they might want help with** — typical assistant tasks around this concept.
+## CauseStarter API map (jobs → SDK / HTTP)
+
+[Full page](/docs/end-user/causestarter/for-llms.md)
+
+- **What it is:** Task index for agents: which `@commonality/sdk` subpath (including IPFS upload/fetch) and which HTTP services (cause-assist, implication attester, nudgers) implement each job. Points at generated TypeDoc under `/api-docs/sdk/` rather than restating signatures. Documents the repo MCP (`mcp/`).
+- **When a user encounters it:** `/docs/for-llms`, or when an assistant is about to call the protocol instead of driving the UI.
+- **What they might want help with:** Uploading a statement to IPFS; signing; reading a cause ref; talking to an attester without treating it as platform truth.
+
## CauseStarter (you landed on a cause page)
[Full page](/docs/end-user/causestarter/index.md)
diff --git a/mcp/README.md b/mcp/README.md
new file mode 100644
index 00000000..2b1a77c2
--- /dev/null
+++ b/mcp/README.md
@@ -0,0 +1,47 @@
+# Commonality MCP
+
+Stdio [MCP](https://modelcontextprotocol.io) server for agents that should talk to the protocol instead of scraping CauseStarter.
+
+Reads go through `@commonality/sdk` (event cache + IPFS + folds). Optional HTTP helpers reach **cause-assist** and the **implication attester**. Chain/IPFS writes stay off until `COMMONALITY_MCP_WRITES=1`.
+
+Job map: [`docs/end-user/causestarter/for-llms.md`](../docs/end-user/causestarter/for-llms.md). Generated SDK docs: `sdk/docs/api/` (`npm run build:docs`).
+
+## Run
+
+From the repo root, with the same env as local stack (`.env` after `./scripts/setup-env.sh localhost`):
+
+```bash
+npm run mcp --workspace=@commonality/mcp
+```
+
+Grok (`~/.grok/config.toml` or `grok mcp add`):
+
+```toml
+[mcp_servers.commonality]
+command = "npm"
+args = ["run", "mcp", "--workspace=@commonality/mcp"]
+```
+
+Run the process with cwd = this repository so workspace resolution works.
+
+## Tools
+
+| Tool | Side effect |
+| --- | --- |
+| `get_statement`, `fetch_ipfs`, `get_implications_*`, `get_user_belief`, `get_user_ref`, `get_project`, `indexer_status` | Read |
+| `cause_assist` | HTTP to cause-assist (wording only) |
+| `implication_attester_status`, `evaluate_implication` | HTTP; evaluate may 402 (x402) and, if paid, the *attester* publishes on-chain |
+| `upload_ipfs`, `believe_statement` | Writes; require `COMMONALITY_MCP_WRITES=1` |
+
+## Env
+
+Same contract/IPFS/indexer names as integration tests (`EVENT_CACHE_URL`, `IPFS_API`, `IPFS_GATEWAY`, `BELIEFS_CONTRACT_ADDRESS`, …). Extra:
+
+| Var | Default |
+| --- | --- |
+| `CAUSE_ASSIST_URL` | `http://127.0.0.1:3002` |
+| `IMPLICATION_ATTESTER_URL` | `http://localhost:3006/implication-attester` |
+| `COMMONALITY_MCP_WRITES` | unset (reads only) |
+| `MCP_PRIVATE_KEY` or `ETHEREUM_PRIVATE_KEY` | required for `believe_statement` |
+
+Do not log secrets. Stdio is the MCP transport — keep `console.log` off `stdout`.
diff --git a/mcp/eslint.config.js b/mcp/eslint.config.js
new file mode 100644
index 00000000..378deec3
--- /dev/null
+++ b/mcp/eslint.config.js
@@ -0,0 +1,33 @@
+import js from '@eslint/js'
+import codeMetrics from '../eslint.metrics.mjs'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ ...codeMetrics,
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: {
+ node: true,
+ mocha: true,
+ },
+ parserOptions: {
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-unused-vars': ['error', {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ }],
+ '@typescript-eslint/no-explicit-any': 'off',
+ },
+ },
+])
diff --git a/mcp/package.json b/mcp/package.json
new file mode 100644
index 00000000..c37c6ec9
--- /dev/null
+++ b/mcp/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@commonality/mcp",
+ "version": "0.1.0",
+ "description": "Stdio MCP server for Commonality SDK reads, IPFS, and helper HTTP services",
+ "type": "module",
+ "bin": {
+ "commonality-mcp": "./dist/index.js"
+ },
+ "scripts": {
+ "prebuild": "npm run build --workspace=@commonality/sdk",
+ "build": "tsc",
+ "clean": "rm -rf dist",
+ "pretypecheck": "npm run build --workspace=@commonality/sdk",
+ "typecheck": "tsc --noEmit",
+ "mcp": "tsx src/index.ts",
+ "start": "node dist/index.js",
+ "test": "npm run build && mocha \"dist/**/*.test.js\"",
+ "lint": "eslint ."
+ },
+ "dependencies": {
+ "@commonality/sdk": "1.0.0",
+ "@modelcontextprotocol/sdk": "^1.25.1",
+ "viem": "2.54.3",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^20.10.0",
+ "eslint": "^9.39.1",
+ "mocha": "^10.8.2",
+ "tsx": "^4.21.0",
+ "typescript": "^5.3.2",
+ "typescript-eslint": "^8.46.4"
+ }
+}
diff --git a/mcp/src/config.ts b/mcp/src/config.ts
new file mode 100644
index 00000000..e66cd990
--- /dev/null
+++ b/mcp/src/config.ts
@@ -0,0 +1,65 @@
+import type { Address } from 'viem'
+import type { ContractAddresses } from '@commonality/sdk/machinery'
+
+function env(name: string, fallback?: string): string | undefined {
+ const value = process.env[name]
+ if (value && value.length > 0) return value
+ return fallback
+}
+
+function address(name: string): Address | undefined {
+ const value = env(name)
+ return value ? (value as Address) : undefined
+}
+
+export const CAUSE_ASSIST_PATHS = [
+ '/atomize',
+ '/sharpen-plank',
+ '/draft-anchor',
+ '/suggest-statements',
+ '/suggest-mediator-scaffold',
+ '/draft-modified-plank',
+ '/draft-stand-in-sliver',
+ '/draft-bridge-plank',
+ '/critique-triple',
+ '/check-implications',
+ '/safety-check',
+ '/check-coherence',
+ '/health',
+] as const
+
+export type CauseAssistPath = (typeof CAUSE_ASSIST_PATHS)[number]
+
+export function isCauseAssistPath(path: string): path is CauseAssistPath {
+ return (CAUSE_ASSIST_PATHS as readonly string[]).includes(path)
+}
+
+const ZERO: Address = '0x0000000000000000000000000000000000000000'
+
+function loadContractAddresses(): ContractAddresses {
+ return {
+ beliefs: address('BELIEFS_CONTRACT_ADDRESS') ?? ZERO,
+ implications: address('IMPLICATIONS_CONTRACT_ADDRESS') ?? ZERO,
+ assuranceContractFactory: address('ASSURANCE_CONTRACT_FACTORY_ADDRESS') ?? ZERO,
+ erc1155Factory: address('ERC1155_FACTORY_ADDRESS') ?? ZERO,
+ delegatableNotes: address('DELEGATABLE_NOTES_CONTRACT_ADDRESS') ?? address('DELEGATABLE_NOTES_ADDRESS') ?? ZERO,
+ noteIntent: address('NOTE_INTENT_ADDRESS') ?? ZERO,
+ alignmentAttestations: address('PROJECT_ALIGNMENT_CONTRACT_ADDRESS') ?? address('ALIGNMENT_ATTESTATIONS_CONTRACT_ADDRESS') ?? ZERO,
+ mutableRefUpdater: address('MUTABLE_REF_UPDATER_CONTRACT_ADDRESS') ?? address('MUTABLE_REF_UPDATER_ADDRESS') ?? ZERO,
+ trustRegistry: address('TRUST_REGISTRY_ADDRESS') ?? ZERO,
+ nudgePublications: address('NUDGE_PUBLICATIONS_CONTRACT_ADDRESS'),
+ publishedData: address('PUBLISHED_DATA_CONTRACT_ADDRESS'),
+ }
+}
+
+export function loadMcpConfig() {
+ return {
+ writesEnabled: process.env.COMMONALITY_MCP_WRITES === '1',
+ privateKey: (env('MCP_PRIVATE_KEY') ?? env('ETHEREUM_PRIVATE_KEY') ?? env('PRIVATE_KEY')) as `0x${string}` | undefined,
+ rpcUrl: env('ETH_RPC_URL') ?? env('RPC_URL') ?? 'http://127.0.0.1:8545',
+ eventCacheUrl: env('EVENT_CACHE_URL') ?? 'http://localhost:42069',
+ causeAssistUrl: (env('CAUSE_ASSIST_URL') ?? 'http://127.0.0.1:3002').replace(/\/$/, ''),
+ implicationAttesterUrl: (env('IMPLICATION_ATTESTER_URL') ?? 'http://localhost:3006/implication-attester').replace(/\/$/, ''),
+ contractAddresses: loadContractAddresses(),
+ }
+}
diff --git a/mcp/src/http.ts b/mcp/src/http.ts
new file mode 100644
index 00000000..fab4b608
--- /dev/null
+++ b/mcp/src/http.ts
@@ -0,0 +1,13 @@
+export async function fetchJson(url: string, init?: RequestInit): Promise<{ status: number; body: unknown }> {
+ const response = await fetch(url, init)
+ const text = await response.text()
+ let body: unknown = text
+ if (text.length > 0) {
+ try {
+ body = JSON.parse(text)
+ } catch {
+ body = text
+ }
+ }
+ return { status: response.status, body }
+}
diff --git a/mcp/src/index.ts b/mcp/src/index.ts
new file mode 100644
index 00000000..8fd21df3
--- /dev/null
+++ b/mcp/src/index.ts
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
+import { registerCommonalityTools } from './tools.js'
+
+const server = new McpServer({
+ name: 'commonality',
+ version: '0.1.0',
+})
+
+registerCommonalityTools(server)
+
+const transport = new StdioServerTransport()
+await server.connect(transport)
diff --git a/mcp/src/json.test.ts b/mcp/src/json.test.ts
new file mode 100644
index 00000000..0db05446
--- /dev/null
+++ b/mcp/src/json.test.ts
@@ -0,0 +1,32 @@
+import assert from 'node:assert/strict'
+import { jsonText } from './json.js'
+import { isCauseAssistPath, loadMcpConfig } from './config.js'
+import { TOOL_NAMES } from './tools.js'
+
+describe('mcp helpers', () => {
+ it('serializes bigint as a decimal string', () => {
+ assert.equal(jsonText({ n: 1n }), '{\n "n": "1"\n}')
+ })
+
+ it('allow-lists cause-assist paths and rejects others', () => {
+ assert.equal(isCauseAssistPath('/atomize'), true)
+ assert.equal(isCauseAssistPath('/evaluate-implication'), false)
+ })
+
+ it('keeps writes off unless COMMONALITY_MCP_WRITES=1', () => {
+ const previous = process.env.COMMONALITY_MCP_WRITES
+ delete process.env.COMMONALITY_MCP_WRITES
+ assert.equal(loadMcpConfig().writesEnabled, false)
+ process.env.COMMONALITY_MCP_WRITES = '1'
+ assert.equal(loadMcpConfig().writesEnabled, true)
+ if (previous === undefined) delete process.env.COMMONALITY_MCP_WRITES
+ else process.env.COMMONALITY_MCP_WRITES = previous
+ })
+
+ it('exports the expected tool names', () => {
+ assert.ok(TOOL_NAMES.includes('fetch_ipfs'))
+ assert.ok(TOOL_NAMES.includes('upload_ipfs'))
+ assert.ok(TOOL_NAMES.includes('cause_assist'))
+ assert.ok(TOOL_NAMES.includes('evaluate_implication'))
+ })
+})
diff --git a/mcp/src/json.ts b/mcp/src/json.ts
new file mode 100644
index 00000000..b4a15241
--- /dev/null
+++ b/mcp/src/json.ts
@@ -0,0 +1,11 @@
+export function jsonText(value: unknown): string {
+ return JSON.stringify(value, (_key, item) => (typeof item === 'bigint' ? item.toString() : item), 2)
+}
+
+export function textResult(value: unknown): { content: Array<{ type: 'text'; text: string }> } {
+ return { content: [{ type: 'text', text: jsonText(value) }] }
+}
+
+export function errorResult(message: string): { content: Array<{ type: 'text'; text: string }>; isError: true } {
+ return { content: [{ type: 'text', text: message }], isError: true }
+}
diff --git a/mcp/src/machinery.ts b/mcp/src/machinery.ts
new file mode 100644
index 00000000..55c42507
--- /dev/null
+++ b/mcp/src/machinery.ts
@@ -0,0 +1,27 @@
+import { createPublicClient, http } from 'viem'
+import { hardhat } from 'viem/chains'
+import { createSDKMachinery, type SDKMachinery } from '@commonality/sdk/machinery'
+import { createIPFSConfigInNodeJSFromTheUsualEnvVars } from '@commonality/sdk/node'
+import { loadMcpConfig } from './config.js'
+
+let cached: SDKMachinery | undefined
+
+export function getMachinery(): SDKMachinery {
+ if (cached) return cached
+ const config = loadMcpConfig()
+ const publicClient = createPublicClient({
+ chain: hardhat,
+ transport: http(config.rpcUrl),
+ })
+ cached = createSDKMachinery({
+ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars(),
+ publicClient,
+ eventCacheUrl: config.eventCacheUrl,
+ contractAddresses: config.contractAddresses,
+ })
+ return cached
+}
+
+export function resetMachineryForTests(): void {
+ cached = undefined
+}
diff --git a/mcp/src/tools.ts b/mcp/src/tools.ts
new file mode 100644
index 00000000..2f967ed7
--- /dev/null
+++ b/mcp/src/tools.ts
@@ -0,0 +1,223 @@
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
+import { z } from 'zod'
+import type { IpfsCidV1 } from '@commonality/sdk/utils'
+import { fetchFromIPFS, uploadToIPFS, createWriteClients } from '@commonality/sdk/utils'
+import { getStatementWithContent, getImplicationsFrom, getImplicationsTo, getUserBelief, believeStatement } from '@commonality/sdk/conceptspace'
+import { getUserRef } from '@commonality/sdk/mutable-refs'
+import { getProject } from '@commonality/sdk/lazy-giving'
+import { BeliefsAbi } from '@commonality/sdk/abis'
+import { errorResult, textResult } from './json.js'
+import { isCauseAssistPath, loadMcpConfig } from './config.js'
+import { fetchJson } from './http.js'
+import { getMachinery } from './machinery.js'
+
+function asCid(cid: string): IpfsCidV1 {
+ return cid as IpfsCidV1
+}
+
+function requireWrites() {
+ const config = loadMcpConfig()
+ if (!config.writesEnabled) {
+ throw new Error('Writes are disabled. Set COMMONALITY_MCP_WRITES=1 to enable upload_ipfs and believe_statement.')
+ }
+ return config
+}
+
+export const TOOL_NAMES = [
+ 'get_statement',
+ 'fetch_ipfs',
+ 'get_implications_from',
+ 'get_implications_to',
+ 'get_user_belief',
+ 'get_user_ref',
+ 'get_project',
+ 'indexer_status',
+ 'cause_assist',
+ 'implication_attester_status',
+ 'evaluate_implication',
+ 'upload_ipfs',
+ 'believe_statement',
+] as const
+
+export function registerCommonalityTools(server: McpServer): void {
+ registerReadTools(server)
+ registerHttpTools(server)
+ registerWriteTools(server)
+}
+
+function registerReadTools(server: McpServer): void {
+ server.tool(
+ 'get_statement',
+ 'Read a statement CID: on-chain belief counts plus displayable IPFS/PublishedData content.',
+ { cid: z.string().describe('IPFS CID of the statement') },
+ async ({ cid }) => {
+ const statement = await getStatementWithContent(getMachinery(), asCid(cid))
+ return textResult(statement)
+ },
+ )
+
+ server.tool(
+ 'fetch_ipfs',
+ 'Fetch JSON from the configured IPFS gateway (SDK fetchFromIPFS).',
+ { cid: z.string() },
+ async ({ cid }) => {
+ const content = await fetchFromIPFS(getMachinery().ipfsConfig, cid)
+ return textResult(content)
+ },
+ )
+
+ server.tool(
+ 'get_implications_from',
+ 'On-chain implication attestations originating from this statement CID.',
+ {
+ cid: z.string(),
+ trustedAttesters: z.array(z.string()).optional(),
+ },
+ async ({ cid, trustedAttesters }) => {
+ const rows = await getImplicationsFrom(getMachinery(), asCid(cid), trustedAttesters)
+ return textResult(rows)
+ },
+ )
+
+ server.tool(
+ 'get_implications_to',
+ 'On-chain implication attestations pointing at this statement CID.',
+ {
+ cid: z.string(),
+ trustedAttesters: z.array(z.string()).optional(),
+ },
+ async ({ cid, trustedAttesters }) => {
+ const rows = await getImplicationsTo(getMachinery(), asCid(cid), trustedAttesters)
+ return textResult(rows)
+ },
+ )
+
+ server.tool(
+ 'get_user_belief',
+ 'One address’s belief state for one statement CID.',
+ { address: z.string(), cid: z.string() },
+ async ({ address, cid }) => {
+ const belief = await getUserBelief(getMachinery(), address, asCid(cid))
+ return textResult(belief)
+ },
+ )
+
+ server.tool(
+ 'get_user_ref',
+ 'Named mutable ref (cause boards are organizer-owned refs pointing at a roster CID).',
+ { owner: z.string().describe('Organizer address'), name: z.string().describe('Ref name / slug') },
+ async ({ owner, name }) => {
+ const ref = await getUserRef(getMachinery(), owner, name)
+ return textResult(ref)
+ },
+ )
+
+ server.tool(
+ 'get_project',
+ 'Fold a LazyGiving assurance-contract address into project state.',
+ { address: z.string().describe('Assurance contract address') },
+ async ({ address }) => {
+ const project = await getProject(getMachinery(), address)
+ return textResult(project)
+ },
+ )
+
+}
+
+function registerHttpTools(server: McpServer): void {
+ server.tool(
+ 'indexer_status',
+ 'GET the event-cache /status endpoint.',
+ async () => {
+ const { eventCacheUrl } = loadMcpConfig()
+ return textResult(await fetchJson(`${eventCacheUrl}/status`))
+ },
+ )
+
+ server.tool(
+ 'cause_assist',
+ 'Call Cause Assist wording helpers (no chain writes). path must be an allow-listed endpoint.',
+ {
+ path: z.string().describe('e.g. /atomize, /sharpen-plank, /health'),
+ body: z.record(z.unknown()).optional(),
+ },
+ async ({ path, body }) => {
+ if (!isCauseAssistPath(path)) {
+ return errorResult(`Unsupported cause-assist path: ${path}`)
+ }
+ const { causeAssistUrl } = loadMcpConfig()
+ const method = path === '/health' ? 'GET' : 'POST'
+ return textResult(await fetchJson(`${causeAssistUrl}${path}`, {
+ method,
+ headers: method === 'POST' ? { 'content-type': 'application/json' } : undefined,
+ body: method === 'POST' ? JSON.stringify(body ?? {}) : undefined,
+ }))
+ },
+ )
+
+ server.tool(
+ 'implication_attester_status',
+ 'Implication attester GET /health (and /quote if present).',
+ async () => {
+ const { implicationAttesterUrl } = loadMcpConfig()
+ const health = await fetchJson(`${implicationAttesterUrl}/health`)
+ const quote = await fetchJson(`${implicationAttesterUrl}/quote`)
+ return textResult({ health, quote })
+ },
+ )
+
+ server.tool(
+ 'evaluate_implication',
+ 'POST /evaluate-implication on the implication attester. May return HTTP 402 (x402 payment). Positive results publish on-chain as that attester identity, not as the user.',
+ { fromStatementCid: z.string(), toStatementCid: z.string() },
+ async ({ fromStatementCid, toStatementCid }) => {
+ const { implicationAttesterUrl } = loadMcpConfig()
+ return textResult(await fetchJson(`${implicationAttesterUrl}/evaluate-implication`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ fromStatementCid, toStatementCid }),
+ }))
+ },
+ )
+
+}
+
+function registerWriteTools(server: McpServer): void {
+ server.tool(
+ 'upload_ipfs',
+ 'Upload a JSON object to IPFS via the SDK (requires IPFS_API and COMMONALITY_MCP_WRITES=1).',
+ { content: z.record(z.unknown()) },
+ async ({ content }) => {
+ try {
+ requireWrites()
+ const cid = await uploadToIPFS(getMachinery().ipfsConfig, content)
+ return textResult({ cid })
+ } catch (error) {
+ return errorResult(error instanceof Error ? error.message : String(error))
+ }
+ },
+ )
+
+ server.tool(
+ 'believe_statement',
+ 'On-chain setBelief(BELIEVES) for a statement CID. Requires COMMONALITY_MCP_WRITES=1 and MCP_PRIVATE_KEY (or ETHEREUM_PRIVATE_KEY).',
+ { cid: z.string() },
+ async ({ cid }) => {
+ try {
+ const config = requireWrites()
+ if (!config.privateKey) {
+ throw new Error('Set MCP_PRIVATE_KEY or ETHEREUM_PRIVATE_KEY for writes.')
+ }
+ const clients = createWriteClients(config.privateKey, config.rpcUrl)
+ const hash = await believeStatement(
+ clients,
+ { address: config.contractAddresses.beliefs, abi: BeliefsAbi },
+ asCid(cid),
+ )
+ return textResult({ transactionHash: hash })
+ } catch (error) {
+ return errorResult(error instanceof Error ? error.message : String(error))
+ }
+ },
+ )
+}
diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json
new file mode 100644
index 00000000..1bbc55a4
--- /dev/null
+++ b/mcp/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "dist",
+ "rootDir": "src",
+ "declaration": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/package-lock.json b/package-lock.json
index cc7ba700..f6ef651c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"alignment-trust-bootstrap",
"fake-data-generation",
"cause-assist",
+ "mcp",
"ui",
"causestarter"
],
@@ -470,6 +471,28 @@
"url": "https://dotenvx.com"
}
},
+ "mcp": {
+ "name": "@commonality/mcp",
+ "version": "0.1.0",
+ "dependencies": {
+ "@commonality/sdk": "1.0.0",
+ "@modelcontextprotocol/sdk": "^1.25.1",
+ "viem": "2.54.3",
+ "zod": "^3.25.76"
+ },
+ "bin": {
+ "commonality-mcp": "dist/index.js"
+ },
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^20.10.0",
+ "eslint": "^9.39.1",
+ "mocha": "^10.8.2",
+ "tsx": "^4.21.0",
+ "typescript": "^5.3.2",
+ "typescript-eslint": "^8.46.4"
+ }
+ },
"node_modules/@aave/account": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@aave/account/-/account-0.2.0.tgz",
@@ -1272,6 +1295,10 @@
"resolved": "services/implication-graph-nudger",
"link": true
},
+ "node_modules/@commonality/mcp": {
+ "resolved": "mcp",
+ "link": true
+ },
"node_modules/@commonality/nudger-core": {
"resolved": "services/nudger-core",
"link": true
@@ -4521,6 +4548,275 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9 || ^2.0.5",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/@hono/node-server": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz",
+ "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/jose": {
+ "version": "6.2.12",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/@motionone/animation": {
"version": "10.18.0",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz",
@@ -12631,6 +12927,59 @@
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
"license": "MIT"
},
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -13873,6 +14222,23 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -15856,6 +16222,27 @@
"node": ">=0.8.x"
}
},
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
+ "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
@@ -15952,6 +16339,25 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express-rate-limit": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
+ "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "ip-address": "^10.2.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
"node_modules/express/node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
@@ -16305,6 +16711,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/find-replace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
@@ -18129,7 +18556,6 @@
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -18520,6 +18946,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -19845,6 +20277,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/media-typer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@@ -20927,6 +21372,35 @@
"node": ">= 4.4.x"
}
},
+ "node_modules/negotiator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/negotiator/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -22231,6 +22705,15 @@
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
@@ -23786,6 +24269,37 @@
"node": ">= 0.6"
}
},
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -24405,6 +24919,32 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/router/node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -27350,6 +27890,62 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/typechain": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz",
@@ -31123,6 +31719,15 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25.28 || ^4"
+ }
+ },
"node_modules/zod-validation-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
diff --git a/package.json b/package.json
index e94ce4b9..9a683d96 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"alignment-trust-bootstrap",
"fake-data-generation",
"cause-assist",
+ "mcp",
"ui",
"causestarter"
],
@@ -45,6 +46,9 @@
"cause-assist:start": "npm run start --workspace=@commonality/cause-assist",
"cause-assist:test": "npm run test --workspace=@commonality/cause-assist",
"cause-assist:typecheck": "npm run typecheck --workspace=@commonality/cause-assist",
+ "mcp": "npm run mcp --workspace=@commonality/mcp",
+ "mcp:test": "npm run test --workspace=@commonality/mcp",
+ "mcp:typecheck": "npm run typecheck --workspace=@commonality/mcp",
"coherence-badge-worker:test": "npm run test --workspace=@commonality/coherence-badge-worker",
"alignment-trust-bootstrap:test": "npm run test --workspace=@commonality/alignment-trust-bootstrap",
"causestarter:typecheck": "npm run typecheck --workspace=ui",
diff --git a/scripts/check-docs-inventory.mjs b/scripts/check-docs-inventory.mjs
index aaa2b7a0..a433c5f1 100644
--- a/scripts/check-docs-inventory.mjs
+++ b/scripts/check-docs-inventory.mjs
@@ -123,6 +123,7 @@ function assertDocumentedPackagePathsExist() {
'ui/README.md',
'indexer/README.md',
'integration-tests/README.md',
+ 'mcp/README.md',
]
for (const file of durablePackageDocs) {
diff --git a/specs/user-docs.md b/specs/user-docs.md
index e5dd94c0..b074b188 100644
--- a/specs/user-docs.md
+++ b/specs/user-docs.md
@@ -29,6 +29,6 @@ Write the docs for humans (narrative, plain language). Then add a block to the d
**User-facing docs live in:**
- Role-based how-tos live on the site where the role is actually performed (e.g. `lazyGiving/get-your-project-funded.md`, `alignment/become-a-delegate.md`, `tally/express-what-you-care-about.md`). The cross-ecosystem index is in [docs/end-user/commonality/index.md](/docs/end-user/commonality/index.md) under "What can I do across the ecosystem?". Each role doc ends with an "On other sites" footer pointing at the cross-site connections.
-- CauseStarter’s in-app docs (`ui/src/causestarter` `/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, and `commonality/`. The spoken briefing is [causestarter/index.md](/docs/end-user/causestarter/index.md) (bulletin board + two twists; the rest is objections). The jobs catalog is [the-jobs.md](/docs/end-user/causestarter/the-jobs.md).
+- CauseStarter’s in-app docs (`ui/src/causestarter` `/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, and `commonality/`. The spoken briefing is [causestarter/index.md](/docs/end-user/causestarter/index.md) (bulletin board + two twists; the rest is objections). The jobs catalog is [the-jobs.md](/docs/end-user/causestarter/the-jobs.md). The agent/protocol index is [for-llms.md](/docs/end-user/causestarter/for-llms.md) (SDK + IPFS + attester HTTP + MCP); generated TypeDoc/contract docs are `/api-docs/sdk/` and `/api-docs/contracts/`.
- [docs/end-user/shared/use-case-walkthroughs/](/docs/end-user/shared/use-case-walkthroughs/README.md) — concrete scenarios
- [docs/end-user/shared/key-ideas/](/docs/end-user/shared/key-ideas/README.md) — concept reference pages
diff --git a/tsconfig.json b/tsconfig.json
index 632ccd6a..d605a3c8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -27,7 +27,8 @@
"integration-tests/src/**/*.ts",
"ui/src/**/*.ts",
"ui/src/**/*.tsx",
- "cause-assist/src/**/*.ts"
+ "cause-assist/src/**/*.ts",
+ "mcp/src/**/*.ts"
],
"exclude": ["node_modules", "dist", ".turbo"]
}
diff --git a/ui/src/causestarter/pages/DocsPage.test.tsx b/ui/src/causestarter/pages/DocsPage.test.tsx
index ba1c97ed..3babace3 100644
--- a/ui/src/causestarter/pages/DocsPage.test.tsx
+++ b/ui/src/causestarter/pages/DocsPage.test.tsx
@@ -32,6 +32,17 @@ describe('DocsPage', () => {
'href',
'/docs/faq',
)
+ expect(screen.getByRole('link', { name: /CauseStarter API map \(for LLMs\)/i })).toHaveAttribute(
+ 'href',
+ '/docs/for-llms',
+ )
+ expect(screen.getByRole('link', { name: /SDK API docs/i })).toHaveAttribute('href', '/api-docs/sdk/')
+ })
+
+ it('renders the LLM API map', () => {
+ renderDocs('/docs/for-llms')
+ expect(screen.getByRole('heading', { name: /CauseStarter API map/i })).toBeInTheDocument()
+ expect(screen.getByText(/fetchFromIPFS/i)).toBeInTheDocument()
})
it('renders the FAQ', () => {
diff --git a/ui/src/causestarter/pages/DocsPage.tsx b/ui/src/causestarter/pages/DocsPage.tsx
index 9287269f..8ddaeb74 100644
--- a/ui/src/causestarter/pages/DocsPage.tsx
+++ b/ui/src/causestarter/pages/DocsPage.tsx
@@ -167,9 +167,9 @@ export function DocsPage() {
),
a: ({ href, children }) => {
const resolved = href ? resolveHref(href, pathForRelativeLinks) : '#'
- if (/^https?:\/\//.test(resolved)) {
+ if (/^https?:\/\//.test(resolved) || resolved.startsWith('/api-docs')) {
return (
-
+
{children}
)
diff --git a/ui/vite.config.ts b/ui/vite.config.ts
index f00ef8ef..95ae70d3 100644
--- a/ui/vite.config.ts
+++ b/ui/vite.config.ts
@@ -1,4 +1,5 @@
-import { mkdirSync, writeFileSync } from 'node:fs'
+import { cpSync, createReadStream, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
+import type { IncomingMessage, ServerResponse } from 'node:http'
import path from 'node:path'
import { defineConfig, loadEnv, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
@@ -18,7 +19,7 @@ export default defineConfig(({ mode }) => {
build: {
outDir: `dist/${domain}`,
},
- plugins: [react(), htmlTitlePlugin(domain), runtimeConfigPlugin(domain, env), endUserDocsPlugin({ domain })],
+ plugins: [react(), htmlTitlePlugin(domain), runtimeConfigPlugin(domain, env), endUserDocsPlugin({ domain }), apiDocsStaticPlugin(domain)],
worker: {
format: 'es',
},
@@ -115,6 +116,64 @@ const DOMAIN_TITLES: Record = {
causestarter: 'CauseStarter',
}
+function sendStaticFile(root: string, urlPath: string, res: ServerResponse, next: () => void) {
+ const rel = decodeURIComponent((urlPath.split('?')[0] ?? '/')).replace(/\\/g, '/')
+ const candidate = path.resolve(root, rel === '/' ? 'index.html' : rel.replace(/^\//, ''))
+ const relative = path.relative(root, candidate)
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
+ next()
+ return
+ }
+ let file = candidate
+ if (existsSync(file) && statSync(file).isDirectory()) {
+ file = path.join(file, 'index.html')
+ }
+ if (!existsSync(file) || !statSync(file).isFile()) {
+ next()
+ return
+ }
+ const ext = path.extname(file)
+ const types: Record = {
+ '.html': 'text/html; charset=utf-8',
+ '.css': 'text/css; charset=utf-8',
+ '.js': 'text/javascript; charset=utf-8',
+ '.json': 'application/json; charset=utf-8',
+ '.md': 'text/markdown; charset=utf-8',
+ '.svg': 'image/svg+xml',
+ }
+ res.setHeader('Content-Type', types[ext] ?? 'application/octet-stream')
+ createReadStream(file).pipe(res)
+}
+
+function apiDocsStaticPlugin(buildDomain: string): Plugin {
+ const sdkDocs = path.resolve(process.cwd(), '../sdk/docs/api')
+ const hardhatDocs = path.resolve(process.cwd(), '../hardhat/docs')
+ const mount = (root: string) =>
+ (req: IncomingMessage, res: ServerResponse, next: () => void) => {
+ if (!existsSync(root)) {
+ next()
+ return
+ }
+ sendStaticFile(root, req.url ?? '/', res, next)
+ }
+ return {
+ name: 'commonality-api-docs-static',
+ configureServer(server) {
+ server.middlewares.use('/api-docs/sdk', mount(sdkDocs))
+ server.middlewares.use('/api-docs/contracts', mount(hardhatDocs))
+ },
+ closeBundle() {
+ const outDir = path.resolve(process.cwd(), 'dist', buildDomain, 'api-docs')
+ if (existsSync(sdkDocs)) {
+ cpSync(sdkDocs, path.join(outDir, 'sdk'), { recursive: true })
+ }
+ if (existsSync(hardhatDocs)) {
+ cpSync(hardhatDocs, path.join(outDir, 'contracts'), { recursive: true })
+ }
+ },
+ }
+}
+
function htmlTitlePlugin(buildDomain: string): Plugin {
const title = DOMAIN_TITLES[buildDomain] ?? 'Commonality'
return {