@toon-protocol/client-mcp 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -86,6 +86,17 @@ interface PublishResponse {
86
86
  channelId: string;
87
87
  /** Channel nonce after this publish (advances by one per paid write). */
88
88
  nonce: number;
89
+ /**
90
+ * The fee actually paid for this write, in base (micro) units, as a decimal
91
+ * string. The truthful amount the claim advanced by — surface this in
92
+ * receipts / text fallback instead of re-reading the per-event estimate.
93
+ */
94
+ feePaid: string;
95
+ /**
96
+ * Available (spendable) channel balance after this write, base units, when
97
+ * known. Lets a receipt / non-rendering host report the post-write balance.
98
+ */
99
+ channelBalanceAfter?: string;
89
100
  }
90
101
  /**
91
102
  * `POST /publish-unsigned` — build, SIGN (with the daemon-held key), and
@@ -1210,6 +1221,13 @@ declare class ClientRunner {
1210
1221
  getTargets(): TargetsResponse;
1211
1222
  /** Pay-to-write a single event through the selected (or default) apex. */
1212
1223
  publish(req: PublishRequest): Promise<PublishResponse>;
1224
+ /**
1225
+ * Available (spendable) balance for a channel after a write — locked collateral
1226
+ * minus cumulative spent, clamped at 0. Same math as {@link getChannels}; used
1227
+ * to report a truthful post-write balance in publish/upload receipts. Returns
1228
+ * undefined if the channel isn't tracked on this apex (balance unknown).
1229
+ */
1230
+ private channelAvailable;
1213
1231
  /**
1214
1232
  * Build, sign (with the daemon-held key), and pay-to-write an event. The
1215
1233
  * caller supplies only the event shell; the private key never leaves the
@@ -1400,11 +1418,29 @@ declare function waitForReady(baseUrl: string, timeoutMs?: number, intervalMs?:
1400
1418
  * "bootstrapping — retry" handling can be unit-tested directly.
1401
1419
  */
1402
1420
 
1421
+ /**
1422
+ * MCP behavioural hints (per the spec's `ToolAnnotations`). Hosts read these to
1423
+ * decide what to auto-run vs gate: free reads can skip approval, paid/irreversible
1424
+ * writes get a confirmation prompt. All hints are advisory, so a host MUST NOT
1425
+ * relax its own gating based on them — but a compliant host CAN use them to gate.
1426
+ */
1427
+ interface ToolAnnotations {
1428
+ /** Does not mutate state — the host may run it without a write prompt. */
1429
+ readOnlyHint?: boolean;
1430
+ /** May perform irreversible/destructive updates (paid writes; channel close). */
1431
+ destructiveHint?: boolean;
1432
+ /** Repeated calls with the same args have no additional effect. */
1433
+ idempotentHint?: boolean;
1434
+ /** Touches an external/open world (relays, chains, the wider web). */
1435
+ openWorldHint?: boolean;
1436
+ }
1403
1437
  /** A JSON-Schema-described MCP tool. */
1404
1438
  interface ToolDefinition {
1405
1439
  name: string;
1406
1440
  description: string;
1407
1441
  inputSchema: Record<string, unknown>;
1442
+ /** Behavioural hints for the host (read-only vs paid/destructive write). */
1443
+ annotations?: ToolAnnotations;
1408
1444
  /** MCP-apps metadata, e.g. `{ ui: { resourceUri } }` linking a UI resource. */
1409
1445
  _meta?: Record<string, unknown>;
1410
1446
  }
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  hasConfiguredIdentity,
9
9
  registerRoutes,
10
10
  scaffoldFirstRun
11
- } from "./chunk-74KX5LXI.js";
11
+ } from "./chunk-EAPVMGLZ.js";
12
12
  import {
13
13
  PUBLISH_TOOL,
14
14
  TOOL_DEFINITIONS,
@@ -18,7 +18,7 @@ import {
18
18
  buildFollowListFilter,
19
19
  buildProfileFilter,
20
20
  dispatchTool
21
- } from "./chunk-LN64MLWP.js";
21
+ } from "./chunk-XVGREVEL.js";
22
22
  import {
23
23
  ControlApiError,
24
24
  ControlClient,
@@ -36,7 +36,7 @@ import {
36
36
  resolveMnemonic,
37
37
  spawnDaemonDetached,
38
38
  waitForReady
39
- } from "./chunk-W6T6ZK7U.js";
39
+ } from "./chunk-AYIP46A2.js";
40
40
  import "./chunk-32QD72IL.js";
41
41
  import "./chunk-DLYE6U2Z.js";
42
42
  import "./chunk-LR7W2ISE.js";
package/dist/mcp.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  APP_RESOURCE_URI,
5
5
  TOOL_DEFINITIONS,
6
6
  dispatchTool
7
- } from "./chunk-LN64MLWP.js";
7
+ } from "./chunk-XVGREVEL.js";
8
8
  import {
9
9
  ARWEAVE_GATEWAYS,
10
10
  ControlClient,
@@ -13,7 +13,7 @@ import {
13
13
  readConfigFile,
14
14
  spawnDaemonDetached,
15
15
  waitForReady
16
- } from "./chunk-W6T6ZK7U.js";
16
+ } from "./chunk-AYIP46A2.js";
17
17
  import "./chunk-32QD72IL.js";
18
18
  import "./chunk-DLYE6U2Z.js";
19
19
  import "./chunk-LR7W2ISE.js";
@@ -23,6 +23,7 @@ import "./chunk-F22GNSF6.js";
23
23
  // src/mcp.ts
24
24
  import { createRequire } from "module";
25
25
  import { readFileSync } from "fs";
26
+ import { createHash } from "crypto";
26
27
  import { dirname, join } from "path";
27
28
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
28
29
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -84,12 +85,20 @@ async function main() {
84
85
  // Travels to EVERY host in the `initialize` result (incl. claude.ai chat,
85
86
  // which never loads the Claude Code skill). Keep the render-first policy
86
87
  // here so it governs all surfaces, not just Claude Code.
87
- instructions: 'TOON is pay-to-write Nostr over Interledger; reads are free, writes are paid. RENDERING IS THE DEFAULT SURFACE: when the user asks to see/show/open/view/browse/render TOON events, profiles, feeds, threads, channels, or balances, call toon_atoms then toon_render (ui://toon/app) \u2014 do NOT route TOON data to a generic HTML/SVG/chart/widget tool or dump it as text. Always call toon_atoms before toon_render. The rendered card IS the response: for read-only views (feeds, profiles, threads, channels, balances, wallet) go STRAIGHT toon_atoms \u2192 toon_render \u2014 do NOT precede a render with daemon-health (toon_status), identity (toon_identity), or balance preflight, and do NOT narrate the tool calls or write a status report; at most a one-line caption after. On ANY intent to upload media or post/publish a picture/video/image (e.g. "I want to upload", "publish an image", "post a photo"), your FIRST action is toon_atoms then toon_render with the media view (media-uploader atom). The uploader has an in-app FILE PICKER, so do NOT ask whether a file is attached, where the image is, or for a URL, and do NOT recount upload-path history or known issues \u2014 just render the uploader and let the user pick a file. Writes (post/like/follow/upload/swap) spend a payment-channel claim; surface the fee and confirm before paying. Fall back to text only on explicit request or render failure.'
88
+ instructions: 'TOON is pay-to-write Nostr over Interledger; reads are free, writes are paid. RENDERING IS THE DEFAULT SURFACE: when the user asks to see/show/open/view/browse/render TOON events, profiles, feeds, threads, channels, or balances, call toon_atoms then toon_render (ui://toon/app) \u2014 do NOT route TOON data to a generic HTML/SVG/chart/widget tool or dump it as text. Always call toon_atoms before toon_render. The rendered card IS the response: for read-only views (feeds, profiles, threads, channels, balances, wallet) go STRAIGHT toon_atoms \u2192 toon_render \u2014 do NOT precede a render with daemon-health (toon_status), identity (toon_identity), or balance preflight, and do NOT narrate the tool calls or write a status report; at most a one-line caption after. On ANY intent to upload media or post/publish a picture/video/image (e.g. "I want to upload", "publish an image", "post a photo"), your FIRST action is toon_atoms then toon_render with the media view (media-uploader atom). The uploader has an in-app FILE PICKER, so do NOT ask whether a file is attached, where the image is, or for a URL, and do NOT recount upload-path history or known issues \u2014 just render the uploader and let the user pick a file. Writes (post/like/follow/upload/swap) spend a payment-channel claim; surface the fee and confirm before paying. When rendering, the trusted host shows the pay/consent surface. When you CANNOT render (a text-only host), you MUST first call toon_status to quote the exact fee + asset, tell the user the fee and that the write is permanent and non-refundable (events cannot be unpublished), and get explicit confirmation before calling any paid write (toon_publish, toon_publish_unsigned, toon_upload, toon_swap). Fall back to text only on explicit request or render failure.'
88
89
  }
89
90
  );
90
91
  const appHtml = loadAppHtml();
92
+ const appVersion = createHash("sha256").update(appHtml).digest("hex").slice(0, 12);
93
+ const versionedUri = `${APP_RESOURCE_URI}?v=${appVersion}`;
94
+ log(`serving ui resource ${versionedUri} (${appHtml.length} bytes)`);
95
+ const toolsWithVersionedUi = TOOL_DEFINITIONS.map((t) => {
96
+ const ui = t._meta?.["ui"] ?? void 0;
97
+ if (!ui?.resourceUri) return t;
98
+ return { ...t, _meta: { ...t._meta, ui: { ...ui, resourceUri: versionedUri } } };
99
+ });
91
100
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
92
- tools: TOOL_DEFINITIONS
101
+ tools: toolsWithVersionedUi
93
102
  }));
94
103
  const arweaveCspDomains = ARWEAVE_GATEWAYS.flatMap((gateway) => {
95
104
  try {
@@ -107,16 +116,16 @@ async function main() {
107
116
  };
108
117
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
109
118
  resources: [
110
- { uri: APP_RESOURCE_URI, name: "TOON", mimeType: APP_MIME, _meta: { ui: APP_CSP } }
119
+ { uri: versionedUri, name: "TOON", mimeType: APP_MIME, _meta: { ui: APP_CSP } }
111
120
  ]
112
121
  }));
113
122
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
114
- if (request.params.uri !== APP_RESOURCE_URI) {
123
+ if (!request.params.uri.startsWith(APP_RESOURCE_URI)) {
115
124
  throw new Error(`Unknown resource: ${request.params.uri}`);
116
125
  }
117
126
  return {
118
127
  contents: [
119
- { uri: APP_RESOURCE_URI, mimeType: APP_MIME, text: appHtml, _meta: { ui: APP_CSP } }
128
+ { uri: request.params.uri, mimeType: APP_MIME, text: appHtml, _meta: { ui: APP_CSP } }
120
129
  ]
121
130
  };
122
131
  });
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `toon-mcp` — a thin MCP stdio server exposing the TOON client to a Claude\n * agent (Desktop or Code). It holds NO chain keys and NO long-lived\n * connections: every tool maps to an HTTP call against the always-on\n * `toon-clientd` daemon, which it auto-spawns (detached) if it is not running.\n *\n * Works on both surfaces:\n * • Claude Desktop — `claude_desktop_config.json` mcpServers entry.\n * • Claude Code — `claude mcp add toon -- toon-mcp` (or `.mcp.json`).\n */\n\nimport { createRequire } from 'node:module';\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n type CallToolResult,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { APP_RESOURCE_URI } from '@toon-protocol/views';\nimport { ARWEAVE_GATEWAYS } from '@toon-protocol/arweave';\nimport { ControlClient } from './control-client.js';\nimport { dispatchTool, TOOL_DEFINITIONS } from './mcp-tools.js';\n\n/** MIME marking the bundle as an MCP-app UI resource (ext-apps profile). */\nconst APP_MIME = 'text/html;profile=mcp-app';\n\n/** Load the prebuilt single-file MCP-app bundle served as `ui://toon/app`. */\nfunction loadAppHtml(): string {\n // 1. Prefer the copy shipped next to the built server (tsup onSuccess copies\n // it into dist/app). This is what a published client-mcp serves — no\n // dependency on the unpublished @toon-protocol/views package at runtime.\n try {\n return readFileSync(new URL('./app/index.html', import.meta.url), 'utf8');\n } catch {\n /* running from source / not yet copied — fall through to the dev resolve */\n }\n // 2. Dev fallback: resolve the bundle from the @toon-protocol/views workspace.\n try {\n const req = createRequire(import.meta.url);\n const entry = req.resolve('@toon-protocol/views'); // …/views/dist/index.js\n return readFileSync(join(dirname(entry), 'app', 'index.html'), 'utf8');\n } catch {\n return '<!doctype html><html><body><div id=\"root\">toon app bundle missing — run `pnpm --filter @toon-protocol/views build`</div></body></html>';\n }\n}\nimport { defaultConfigPath, readConfigFile } from './daemon/config.js';\nimport {\n isDaemonRunning,\n spawnDaemonDetached,\n waitForReady,\n} from './daemon/lifecycle.js';\n\n/** stdout carries the MCP protocol — all logging must go to stderr. */\nfunction log(msg: string): void {\n console.error(`[toon-mcp] ${msg}`);\n}\n\n/** Resolve the daemon control-plane URL without needing the mnemonic. */\nfunction controlPlaneUrl(): string {\n const file = readConfigFile(\n process.env['TOON_CLIENT_CONFIG'] ?? defaultConfigPath()\n );\n const port = Number(\n process.env['TOON_CLIENT_HTTP_PORT'] ?? file.httpPort ?? 8787\n );\n return `http://127.0.0.1:${port}`;\n}\n\n/**\n * Make sure the daemon is up: if the lock shows it running, return; otherwise\n * spawn it detached and wait until the control plane is reachable. Best-effort\n * — failures surface as readable tool errors rather than crashing the server.\n */\nasync function ensureDaemon(url: string): Promise<void> {\n if (isDaemonRunning()) return;\n const client = new ControlClient({ baseUrl: url });\n if (await client.ping()) return;\n log('daemon not running — spawning detached');\n try {\n const pid = spawnDaemonDetached();\n log(`spawned toon-clientd (pid ${pid}); waiting for control plane`);\n await waitForReady(url, 20_000);\n } catch (err) {\n log(\n `failed to spawn daemon: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\nasync function main(): Promise<void> {\n const url = controlPlaneUrl();\n const control = new ControlClient({ baseUrl: url });\n\n // Kick off daemon startup; don't block server init on it (BTP bootstrap can\n // take a moment). Tools report \"bootstrapping — retry\" until it is ready.\n void ensureDaemon(url);\n\n const server = new Server(\n { name: 'toon-client', version: '0.1.0' },\n {\n capabilities: { tools: {}, resources: {} },\n // Travels to EVERY host in the `initialize` result (incl. claude.ai chat,\n // which never loads the Claude Code skill). Keep the render-first policy\n // here so it governs all surfaces, not just Claude Code.\n instructions:\n 'TOON is pay-to-write Nostr over Interledger; reads are free, writes ' +\n 'are paid. RENDERING IS THE DEFAULT SURFACE: when the user asks to ' +\n 'see/show/open/view/browse/render TOON events, profiles, feeds, ' +\n 'threads, channels, or balances, call toon_atoms then toon_render ' +\n '(ui://toon/app) — do NOT route TOON data to a generic ' +\n 'HTML/SVG/chart/widget tool or dump it as text. Always call toon_atoms ' +\n 'before toon_render. The rendered card IS the response: for read-only ' +\n 'views (feeds, profiles, threads, channels, balances, wallet) go ' +\n 'STRAIGHT toon_atoms → toon_render — do NOT precede a render with ' +\n 'daemon-health (toon_status), identity (toon_identity), or balance ' +\n 'preflight, and do NOT narrate the tool calls or write a status ' +\n 'report; at most a one-line caption after. On ANY intent to upload ' +\n 'media or post/publish a ' +\n 'picture/video/image (e.g. \"I want to upload\", \"publish an image\", ' +\n '\"post a photo\"), your FIRST action is toon_atoms then toon_render with ' +\n 'the media view (media-uploader atom). The uploader has an in-app FILE ' +\n 'PICKER, so do NOT ask whether a file is attached, where the image is, ' +\n 'or for a URL, and do NOT recount upload-path history or known issues — ' +\n 'just render the uploader and let the user pick a file. Writes ' +\n '(post/like/follow/upload/swap) spend a ' +\n 'payment-channel claim; surface the fee and confirm before paying. ' +\n 'Fall back to text only on explicit request or render failure.',\n }\n );\n\n const appHtml = loadAppHtml();\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOL_DEFINITIONS,\n }));\n\n // The MCP-app UI resource the host renders for toon_render results.\n //\n // CSP: the rendered feed/uploader shows media stored on Arweave. Without an\n // explicit `resourceDomains`, the host iframe's default `img-src`/`media-src`\n // blocks those gateways and images never load (toon-client#127). Advertise the\n // Arweave gateways as both resource (img/media/static) and connect origins.\n // Per the ext-apps spec the host reads `_meta.ui.csp` from the `resources/read`\n // content item, with the `resources/list` entry as fallback — so set it on both.\n //\n // CRUCIAL: ar.io / arweave.net gateways serve a 302 from the apex\n // (`https://arweave.net/<txId>`) to a per-tx SANDBOX SUBDOMAIN\n // (`https://<base32>.arweave.net/<txId>`). CSP `img-src` is checked against the\n // REDIRECT TARGET, so an apex-only allowlist still blocks the image. Allow both\n // the apex (initial request) and a wildcard subdomain (where the bytes load).\n const arweaveCspDomains = ARWEAVE_GATEWAYS.flatMap((gateway) => {\n try {\n const host = new URL(gateway).host;\n return [gateway, `https://*.${host}`];\n } catch {\n return [gateway];\n }\n });\n const APP_CSP = {\n csp: {\n resourceDomains: arweaveCspDomains,\n connectDomains: arweaveCspDomains,\n },\n };\n\n server.setRequestHandler(ListResourcesRequestSchema, async () => ({\n resources: [\n { uri: APP_RESOURCE_URI, name: 'TOON', mimeType: APP_MIME, _meta: { ui: APP_CSP } },\n ],\n }));\n\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n if (request.params.uri !== APP_RESOURCE_URI) {\n throw new Error(`Unknown resource: ${request.params.uri}`);\n }\n return {\n contents: [\n { uri: APP_RESOURCE_URI, mimeType: APP_MIME, text: appHtml, _meta: { ui: APP_CSP } },\n ],\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n // If the daemon went away, try once to bring it back before dispatching.\n if (!(await control.ping())) await ensureDaemon(url);\n // Our ToolResult is a structural subset of CallToolResult (content + isError);\n // the SDK's handler union also carries a task-augmented variant we never use.\n return (await dispatchTool(control, name, args)) as CallToolResult;\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n log(`ready; proxying to ${url}`);\n}\n\nmain().catch((err) => {\n log(err instanceof Error ? (err.stack ?? err.message) : String(err));\n process.exitCode = 1;\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `toon-mcp` — a thin MCP stdio server exposing the TOON client to a Claude\n * agent (Desktop or Code). It holds NO chain keys and NO long-lived\n * connections: every tool maps to an HTTP call against the always-on\n * `toon-clientd` daemon, which it auto-spawns (detached) if it is not running.\n *\n * Works on both surfaces:\n * • Claude Desktop — `claude_desktop_config.json` mcpServers entry.\n * • Claude Code — `claude mcp add toon -- toon-mcp` (or `.mcp.json`).\n */\n\nimport { createRequire } from 'node:module';\nimport { readFileSync } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { dirname, join } from 'node:path';\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n type CallToolResult,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { APP_RESOURCE_URI } from '@toon-protocol/views';\nimport { ARWEAVE_GATEWAYS } from '@toon-protocol/arweave';\nimport { ControlClient } from './control-client.js';\nimport { dispatchTool, TOOL_DEFINITIONS } from './mcp-tools.js';\n\n/** MIME marking the bundle as an MCP-app UI resource (ext-apps profile). */\nconst APP_MIME = 'text/html;profile=mcp-app';\n\n/** Load the prebuilt single-file MCP-app bundle served as `ui://toon/app`. */\nfunction loadAppHtml(): string {\n // 1. Prefer the copy shipped next to the built server (tsup onSuccess copies\n // it into dist/app). This is what a published client-mcp serves — no\n // dependency on the unpublished @toon-protocol/views package at runtime.\n try {\n return readFileSync(new URL('./app/index.html', import.meta.url), 'utf8');\n } catch {\n /* running from source / not yet copied — fall through to the dev resolve */\n }\n // 2. Dev fallback: resolve the bundle from the @toon-protocol/views workspace.\n try {\n const req = createRequire(import.meta.url);\n const entry = req.resolve('@toon-protocol/views'); // …/views/dist/index.js\n return readFileSync(join(dirname(entry), 'app', 'index.html'), 'utf8');\n } catch {\n return '<!doctype html><html><body><div id=\"root\">toon app bundle missing — run `pnpm --filter @toon-protocol/views build`</div></body></html>';\n }\n}\nimport { defaultConfigPath, readConfigFile } from './daemon/config.js';\nimport {\n isDaemonRunning,\n spawnDaemonDetached,\n waitForReady,\n} from './daemon/lifecycle.js';\n\n/** stdout carries the MCP protocol — all logging must go to stderr. */\nfunction log(msg: string): void {\n console.error(`[toon-mcp] ${msg}`);\n}\n\n/** Resolve the daemon control-plane URL without needing the mnemonic. */\nfunction controlPlaneUrl(): string {\n const file = readConfigFile(\n process.env['TOON_CLIENT_CONFIG'] ?? defaultConfigPath()\n );\n const port = Number(\n process.env['TOON_CLIENT_HTTP_PORT'] ?? file.httpPort ?? 8787\n );\n return `http://127.0.0.1:${port}`;\n}\n\n/**\n * Make sure the daemon is up: if the lock shows it running, return; otherwise\n * spawn it detached and wait until the control plane is reachable. Best-effort\n * — failures surface as readable tool errors rather than crashing the server.\n */\nasync function ensureDaemon(url: string): Promise<void> {\n if (isDaemonRunning()) return;\n const client = new ControlClient({ baseUrl: url });\n if (await client.ping()) return;\n log('daemon not running — spawning detached');\n try {\n const pid = spawnDaemonDetached();\n log(`spawned toon-clientd (pid ${pid}); waiting for control plane`);\n await waitForReady(url, 20_000);\n } catch (err) {\n log(\n `failed to spawn daemon: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\nasync function main(): Promise<void> {\n const url = controlPlaneUrl();\n const control = new ControlClient({ baseUrl: url });\n\n // Kick off daemon startup; don't block server init on it (BTP bootstrap can\n // take a moment). Tools report \"bootstrapping — retry\" until it is ready.\n void ensureDaemon(url);\n\n const server = new Server(\n { name: 'toon-client', version: '0.1.0' },\n {\n capabilities: { tools: {}, resources: {} },\n // Travels to EVERY host in the `initialize` result (incl. claude.ai chat,\n // which never loads the Claude Code skill). Keep the render-first policy\n // here so it governs all surfaces, not just Claude Code.\n instructions:\n 'TOON is pay-to-write Nostr over Interledger; reads are free, writes ' +\n 'are paid. RENDERING IS THE DEFAULT SURFACE: when the user asks to ' +\n 'see/show/open/view/browse/render TOON events, profiles, feeds, ' +\n 'threads, channels, or balances, call toon_atoms then toon_render ' +\n '(ui://toon/app) — do NOT route TOON data to a generic ' +\n 'HTML/SVG/chart/widget tool or dump it as text. Always call toon_atoms ' +\n 'before toon_render. The rendered card IS the response: for read-only ' +\n 'views (feeds, profiles, threads, channels, balances, wallet) go ' +\n 'STRAIGHT toon_atoms → toon_render — do NOT precede a render with ' +\n 'daemon-health (toon_status), identity (toon_identity), or balance ' +\n 'preflight, and do NOT narrate the tool calls or write a status ' +\n 'report; at most a one-line caption after. On ANY intent to upload ' +\n 'media or post/publish a ' +\n 'picture/video/image (e.g. \"I want to upload\", \"publish an image\", ' +\n '\"post a photo\"), your FIRST action is toon_atoms then toon_render with ' +\n 'the media view (media-uploader atom). The uploader has an in-app FILE ' +\n 'PICKER, so do NOT ask whether a file is attached, where the image is, ' +\n 'or for a URL, and do NOT recount upload-path history or known issues — ' +\n 'just render the uploader and let the user pick a file. Writes ' +\n '(post/like/follow/upload/swap) spend a ' +\n 'payment-channel claim; surface the fee and confirm before paying. ' +\n 'When rendering, the trusted host shows the pay/consent surface. When ' +\n 'you CANNOT render (a text-only host), you MUST first call toon_status ' +\n 'to quote the exact fee + asset, tell the user the fee and that the ' +\n 'write is permanent and non-refundable (events cannot be unpublished), ' +\n 'and get explicit confirmation before calling any paid write ' +\n '(toon_publish, toon_publish_unsigned, toon_upload, toon_swap). ' +\n 'Fall back to text only on explicit request or render failure.',\n }\n );\n\n const appHtml = loadAppHtml();\n\n // CACHE-BUST the UI resource. Hosts (Claude Desktop) PREFETCH and CACHE the\n // `ui://` template keyed by its URI and do not re-fetch it across server\n // restarts — so a rebuilt bundle is never picked up while the URI is constant\n // (toon-client iframe stays stale until a reinstall). Version the URI by a hash\n // of the bundle: every rebuild yields a new URI the host has never cached, so\n // it fetches fresh; an unchanged bundle keeps the same URI (no needless churn).\n const appVersion = createHash('sha256').update(appHtml).digest('hex').slice(0, 12);\n const versionedUri = `${APP_RESOURCE_URI}?v=${appVersion}`;\n log(`serving ui resource ${versionedUri} (${appHtml.length} bytes)`);\n\n // Point toon_render's `_meta.ui.resourceUri` at the versioned URI so the host\n // fetches THIS build's bundle, not a cached one. Other tools pass through.\n const toolsWithVersionedUi = TOOL_DEFINITIONS.map((t) => {\n const ui = (t._meta?.['ui'] ?? undefined) as { resourceUri?: string } | undefined;\n if (!ui?.resourceUri) return t;\n return { ...t, _meta: { ...t._meta, ui: { ...ui, resourceUri: versionedUri } } };\n });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: toolsWithVersionedUi,\n }));\n\n // The MCP-app UI resource the host renders for toon_render results.\n //\n // CSP: the rendered feed/uploader shows media stored on Arweave. Without an\n // explicit `resourceDomains`, the host iframe's default `img-src`/`media-src`\n // blocks those gateways and images never load (toon-client#127). Advertise the\n // Arweave gateways as both resource (img/media/static) and connect origins.\n // Per the ext-apps spec the host reads `_meta.ui.csp` from the `resources/read`\n // content item, with the `resources/list` entry as fallback — so set it on both.\n //\n // CRUCIAL: ar.io / arweave.net gateways serve a 302 from the apex\n // (`https://arweave.net/<txId>`) to a per-tx SANDBOX SUBDOMAIN\n // (`https://<base32>.arweave.net/<txId>`). CSP `img-src` is checked against the\n // REDIRECT TARGET, so an apex-only allowlist still blocks the image. Allow both\n // the apex (initial request) and a wildcard subdomain (where the bytes load).\n const arweaveCspDomains = ARWEAVE_GATEWAYS.flatMap((gateway) => {\n try {\n const host = new URL(gateway).host;\n return [gateway, `https://*.${host}`];\n } catch {\n return [gateway];\n }\n });\n const APP_CSP = {\n csp: {\n resourceDomains: arweaveCspDomains,\n connectDomains: arweaveCspDomains,\n },\n };\n\n server.setRequestHandler(ListResourcesRequestSchema, async () => ({\n resources: [\n { uri: versionedUri, name: 'TOON', mimeType: APP_MIME, _meta: { ui: APP_CSP } },\n ],\n }));\n\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n // Accept the versioned URI and the bare base (in case a host strips the\n // `?v=` query before re-reading) — both resolve to this build's bundle.\n if (!request.params.uri.startsWith(APP_RESOURCE_URI)) {\n throw new Error(`Unknown resource: ${request.params.uri}`);\n }\n return {\n contents: [\n { uri: request.params.uri, mimeType: APP_MIME, text: appHtml, _meta: { ui: APP_CSP } },\n ],\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n // If the daemon went away, try once to bring it back before dispatching.\n if (!(await control.ping())) await ensureDaemon(url);\n // Our ToolResult is a structural subset of CallToolResult (content + isError);\n // the SDK's handler union also carries a task-augmented variant we never use.\n return (await dispatchTool(control, name, args)) as CallToolResult;\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n log(`ready; proxying to ${url}`);\n}\n\nmain().catch((err) => {\n log(err instanceof Error ? (err.stack ?? err.message) : String(err));\n process.exitCode = 1;\n});\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toon-protocol/client-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Always-on local daemon + MCP server letting a Claude agent (Desktop or Code) act as a TOON Protocol client: pay-to-write publishing, free reads, channel/balance management, and swaps.",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Green",
@@ -49,8 +49,8 @@
49
49
  "typescript": "^5.3.0",
50
50
  "vitest": "^1.0.0",
51
51
  "@toon-protocol/arweave": "0.1.1",
52
- "@toon-protocol/client": "0.14.11",
53
- "@toon-protocol/views": "0.12.0"
52
+ "@toon-protocol/client": "0.14.12",
53
+ "@toon-protocol/views": "0.12.2"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">=20"