@chaindaddy/mcp 0.5.0 → 0.6.1

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/README.md CHANGED
@@ -143,7 +143,7 @@ or Developer Beta membership on the wallet that opens the store (otherwise `403
143
143
  | `iap_set_webhook`, `iap_get_webhook`, `iap_test_webhook` | The store's webhook: set it (secret shown once), read its recent deliveries, send a test event |
144
144
 
145
145
  Every write sends an `Idempotency-Key` and returns it as `idempotency_key`; retry with the same key and the
146
- write never applies twice. The orchestration skill is [`skills/creator-store.md`](skills/creator-store.md).
146
+ write never applies twice. The orchestration skill is [`skills/sell-items-in-your-app`](skills/sell-items-in-your-app/SKILL.md).
147
147
 
148
148
  ```
149
149
  "Open a store for my token on Base and add a gold badge for $5"
@@ -151,6 +151,15 @@ write never applies twice. The orchestration skill is [`skills/creator-store.md`
151
151
  "Give every wallet in this list 10 credits"
152
152
  ```
153
153
 
154
+ ## Skills
155
+
156
+ The server serves Chain Daddy's skills, step-by-step workflows such as `launch-a-token` and
157
+ `sell-items-in-your-app`, as MCP prompts (one per skill) and through two read-only tools for clients that
158
+ only call tools: `list_skills` and `read_skill`. They are the files in [`skills/`](skills), the one copy:
159
+ the remote server (`https://api.chaindaddy.io/mcp`) serves the same text, and the
160
+ [chaindaddy plugin](https://gitlab.com/opencrown/chaindaddy-plugins) copies them with its
161
+ `scripts/sync-skills.sh`.
162
+
154
163
  ## Example Conversations
155
164
 
156
165
  ### Search and Discover
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;AAiDH,UAAU,OAAO;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,sBAA6B,CAAC;AAInD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAU7D;AAKD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQ1F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;AAkDH,UAAU,OAAO;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,sBAA6B,CAAC;AAInD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAU7D;AAKD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQ1F"}
package/build/index.js CHANGED
@@ -15,6 +15,7 @@ import { SiweMessage } from "siwe";
15
15
  import nacl from "tweetnacl";
16
16
  import bs58 from "bs58";
17
17
  import { CLI_LOGIN, hasCliLogin, loginFetch } from "./login.js";
18
+ import { loadSkills } from "./skills.js";
18
19
  // Configuration
19
20
  const API_URL = process.env.CHAINDADDY_API_URL || "https://api.chaindaddy.io";
20
21
  // A recommender service you run yourself. The hosted API has none, so with
@@ -277,6 +278,10 @@ const VERSION = createRequire(import.meta.url)("../package.json").version;
277
278
  const server = new McpServer({
278
279
  name: "chaindaddy",
279
280
  version: VERSION,
281
+ }, {
282
+ instructions: "Chain Daddy is a registry of verified token pages: each token's ticker on each chain can carry one registration, held by the wallet that proved it created or controls the token. " +
283
+ "These tools search tickers, launch or import a token, and run its creator store. " +
284
+ "Step-by-step workflows (launching or checking a token, exporting a listing, selling items, sign-in, verifying receipts, Crown Apps) are this server's prompts, and list_skills / read_skill read the same skills: read the matching one before such a task.",
280
285
  });
281
286
  // ============================================================================
282
287
  // TOOLS
@@ -1341,25 +1346,22 @@ server.registerTool("deploy_token", {
1341
1346
  });
1342
1347
  // Tool: list_launch_venues
1343
1348
  //
1344
- // Every contract a token can be launched through, with what a trade pays and
1345
- // where it goes, per $100 traded. Rows with freeCrown=true launch through Chain
1346
- // Daddy and come with a free crown; the others are comparison only. Present BOTH
1347
- // numbers for every venue (trader fee and creator share) — ranking on the
1348
- // creator's share alone favours the launchpad that charges traders most — and do
1349
- // not call any venue "best" or "recommended".
1349
+ // Chain Daddy's own launchpad and what it pays, per chain (ownOnly=true): an
1350
+ // agent never points a person at another launch platform (owner rule,
1351
+ // 2026-10-03). Present BOTH numbers (trader fee and creator share).
1350
1352
  server.registerTool("list_launch_venues", {
1351
- title: "List launch venues",
1352
- description: "List the contracts a token can be launched through (ours and third parties'), ranked by what reaches the creator per $100 traded. Name each one by chains[].contract where present, else by the venue's name — a creator picks a contract, not a product tier. Every economics field is basis points of trade volume (100 = $1 per $100 traded). Show both traderFeeBps and creatorBps for each venue. Venues with freeCrown=true launch through Chain Daddy with a free crown; others are comparison-only.\n\nParams: chain_id (number?) — narrow to one chain (EVM chain id; Solana mainnet 900900), session_token (string?) — a signed-in session sees its own plan's rates\nReturns: {venues: [...], sort, generatedAt}\nErrors: API_ERROR",
1353
+ title: "List Chain Daddy's launch options",
1354
+ description: "Chain Daddy's launchpad and what it pays, per chain. The launchpad (chaindaddy-launchpad) creates the token with its own locked trading pool, on EVM chains and on Solana (Meteora DBC): nothing up front, and the token's registration is free. Every economics field is basis points of trade volume (100 = $1 per $100 traded); show both traderFeeBps (what traders pay) and creatorBps (what reaches the creator). For a plain token with no pool, Chain Daddy's token factory (chaindaddy-classic, a one-time fee, registration included) is the other option. Recommend the launchpad unless the person wants a plain token.\n\nParams: chain_id (number?) — narrow to one chain (EVM chain id; Solana mainnet 900900), session_token (string?) — a signed-in session sees its own plan's rates\nReturns: {venues: [...], sort, generatedAt}\nErrors: API_ERROR",
1353
1355
  inputSchema: {
1354
- chain_id: z.number().optional().describe("Only venues that launch on this chain"),
1356
+ chain_id: z.number().optional().describe("Only this chain"),
1355
1357
  session_token: z.string().optional().describe("Optional session token from authenticate"),
1356
1358
  },
1357
1359
  annotations: { readOnlyHint: true, openWorldHint: false },
1358
1360
  }, async ({ chain_id, session_token }) => {
1359
1361
  try {
1360
1362
  const jwt = session_token ? sessions.get(session_token)?.jwt : undefined;
1361
- const q = chain_id ? `?chainId=${chain_id}` : "";
1362
- const data = await apiCall(`/api/v2/launch/venues${q}`, { jwt });
1363
+ const q = chain_id ? `&chainId=${chain_id}` : "";
1364
+ const data = await apiCall(`/api/v2/launch/venues?ownOnly=true${q}`, { jwt });
1363
1365
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
1364
1366
  }
1365
1367
  catch (error) {
@@ -1369,92 +1371,6 @@ server.registerTool("list_launch_venues", {
1369
1371
  };
1370
1372
  }
1371
1373
  });
1372
- // Tool: build_venue_launch
1373
- server.registerTool("build_venue_launch", {
1374
- title: "Build a third-party launch",
1375
- description: "Build a token launch on a third-party contract (e.g. 'clanker') through Chain Daddy. Returns an unsigned transaction: the caller's OWN wallet — the session wallet — must sign and send transactions[0] on its chainId, then call submit_venue_launch with the hash. Chain Daddy never sends it. Once proven, the token's crown is free.\n\nParams: venue (string), chain_id (number), name (string), symbol (string), image (string?) — ipfs:// or https:// URL, description (string?), session_token (string)\nReturns: {intent: {id, status, ...}, transactions: [{chainId, to, data, value}]}\nErrors: NO_SESSION, SESSION_EXPIRED, NO_JWT, VENUE_UNAVAILABLE, API_ERROR",
1376
- inputSchema: {
1377
- venue: z.string().describe("Venue id from list_launch_venues with freeCrown=true (not chaindaddy-launchpad or chaindaddy-classic)"),
1378
- chain_id: z.number().describe("Chain to launch on — one the venue lists as launchable"),
1379
- name: z.string().describe("Token name"),
1380
- symbol: z.string().describe("Ticker"),
1381
- image: z.string().optional().describe("Token image URL"),
1382
- description: z.string().optional().describe("Token description"),
1383
- session_token: z.string().describe("Session token from authenticate"),
1384
- },
1385
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1386
- }, async ({ venue, chain_id, name, symbol, image, description, session_token }) => {
1387
- try {
1388
- const { session, jwt } = requireSessionJWT(session_token);
1389
- const data = await apiPost(`/api/v2/launch/venues/${encodeURIComponent(venue)}/build`, {
1390
- chainId: chain_id,
1391
- wallet: session.wallet,
1392
- name,
1393
- symbol,
1394
- ...(image ? { image } : {}),
1395
- ...(description ? { description } : {}),
1396
- }, {}, { jwt });
1397
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
1398
- }
1399
- catch (error) {
1400
- const message = error.message;
1401
- const code = message.startsWith("NO_SESSION") ? "NO_SESSION"
1402
- : message.startsWith("SESSION_EXPIRED") ? "SESSION_EXPIRED"
1403
- : message.startsWith("NO_JWT") ? "NO_JWT"
1404
- : /not available|does not launch/.test(message) ? "VENUE_UNAVAILABLE"
1405
- : "API_ERROR";
1406
- return { content: [{ type: "text", text: JSON.stringify({ error: code, message }) }], isError: true };
1407
- }
1408
- });
1409
- // Tool: submit_venue_launch
1410
- server.registerTool("submit_venue_launch", {
1411
- title: "Report a launch transaction",
1412
- description: "Report the transaction hash of a launch built by build_venue_launch, after the session wallet sent it. The server proves the launch from the launchpad's own on-chain record; poll get_venue_launch until status is 'confirmed' (freeCrown=true, tokenAddress set) or 'failed' (failureReason says why).\n\nParams: intent_id (string), tx_hash (string), session_token (string)\nReturns: {id, status, tokenAddress?, freeCrown, failureReason?}\nErrors: NO_SESSION, SESSION_EXPIRED, NO_JWT, API_ERROR",
1413
- inputSchema: {
1414
- intent_id: z.string().describe("intent.id from build_venue_launch"),
1415
- tx_hash: z.string().describe("Hash of the sent launch transaction"),
1416
- session_token: z.string().describe("Session token from authenticate"),
1417
- },
1418
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1419
- }, async ({ intent_id, tx_hash, session_token }) => {
1420
- try {
1421
- const { jwt } = requireSessionJWT(session_token);
1422
- const data = await apiPost(`/api/v2/launch/intents/${encodeURIComponent(intent_id)}/submit`, { txHash: tx_hash }, {}, { jwt });
1423
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
1424
- }
1425
- catch (error) {
1426
- const message = error.message;
1427
- const code = message.startsWith("NO_SESSION") ? "NO_SESSION"
1428
- : message.startsWith("SESSION_EXPIRED") ? "SESSION_EXPIRED"
1429
- : message.startsWith("NO_JWT") ? "NO_JWT"
1430
- : "API_ERROR";
1431
- return { content: [{ type: "text", text: JSON.stringify({ error: code, message }) }], isError: true };
1432
- }
1433
- });
1434
- // Tool: get_venue_launch
1435
- server.registerTool("get_venue_launch", {
1436
- title: "Get a third-party launch",
1437
- description: "Read a third-party launch's state. A submitted launch is re-proven on each read.\n\nParams: intent_id (string), session_token (string)\nReturns: {id, status, tokenAddress?, freeCrown, failureReason?}\nErrors: NO_SESSION, SESSION_EXPIRED, NO_JWT, API_ERROR",
1438
- inputSchema: {
1439
- intent_id: z.string().describe("intent.id from build_venue_launch"),
1440
- session_token: z.string().describe("Session token from authenticate"),
1441
- },
1442
- annotations: { readOnlyHint: true, openWorldHint: false },
1443
- }, async ({ intent_id, session_token }) => {
1444
- try {
1445
- const { jwt } = requireSessionJWT(session_token);
1446
- const data = await apiCall(`/api/v2/launch/intents/${encodeURIComponent(intent_id)}`, { jwt });
1447
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
1448
- }
1449
- catch (error) {
1450
- const message = error.message;
1451
- const code = message.startsWith("NO_SESSION") ? "NO_SESSION"
1452
- : message.startsWith("SESSION_EXPIRED") ? "SESSION_EXPIRED"
1453
- : message.startsWith("NO_JWT") ? "NO_JWT"
1454
- : "API_ERROR";
1455
- return { content: [{ type: "text", text: JSON.stringify({ error: code, message }) }], isError: true };
1456
- }
1457
- });
1458
1374
  // Tool: claim_crown
1459
1375
  server.registerTool("claim_crown", {
1460
1376
  title: "Build a registration claim action",
@@ -1752,6 +1668,13 @@ const iapSkuFields = {
1752
1668
  rule: iapRule.nullable(),
1753
1669
  maxSupply: z.number().int().positive().nullable(),
1754
1670
  maxPerWallet: z.number().int().positive().nullable(),
1671
+ maxAppSpend: z
1672
+ .number()
1673
+ .int()
1674
+ .positive()
1675
+ .max(1_000_000)
1676
+ .nullable()
1677
+ .describe("Consumables only: lets Crown Apps on the token page spend this item for the wallet that holds it (the `iap-consume` app action, no server), at most this many per request. null (the default): no app can, only a store key with iap_consume. Leave it null for anything redeemed for a prize or off-platform value"),
1755
1678
  saleStartsAt: z.string().nullable(),
1756
1679
  saleEndsAt: z.string().nullable(),
1757
1680
  status: z.enum(["draft", "active", "archived"]),
@@ -1837,27 +1760,92 @@ async function iapWrite(noun, method, path, body, auth, session_token, idempoten
1837
1760
  return iapFail(error, key);
1838
1761
  }
1839
1762
  }
1763
+ function parsePage(input) {
1764
+ const raw = input.trim();
1765
+ const isUrl = raw.includes("://") || /^([a-z0-9-]+\.)+[a-z]{2,}(:\d+)?(\/|$)/i.test(raw);
1766
+ let segs = (isUrl ? new URL(raw.includes("://") ? raw : `https://${raw}`).pathname : raw).split("/").filter(Boolean).map((p) => decodeURIComponent(p));
1767
+ if (segs[0] === "_")
1768
+ segs = segs.slice(1);
1769
+ const symbol = (segs[0] ?? "").replace(/^\$/, "").toUpperCase();
1770
+ if (!symbol || symbol.startsWith("_") || segs.length > 2 || /^\d+$/.test(segs[1] ?? "")) {
1771
+ throw new IapError("INVALID_PARAMS", `"${input}" is not a token page: pass its symbol (FLUXGP) or its address (chaindaddy.io/FLUXGP/base), or chain_key with crown_id`, 400);
1772
+ }
1773
+ return { symbol, chain: segs[1] };
1774
+ }
1775
+ async function resolvePage(page, chain_key) {
1776
+ const ref = parsePage(page);
1777
+ const wanted = (ref.chain ?? chain_key ?? "").toLowerCase();
1778
+ const [listed, table] = (await Promise.all([
1779
+ apiCall(`/api/v2/symbols/${enc(ref.symbol)}/registrations`),
1780
+ apiCall("/api/v2/config/chains"),
1781
+ ]));
1782
+ const names = new Map((table.chains ?? []).map((c) => [c.caip2.toLowerCase(), c]));
1783
+ const keyOf = (r) => names.get(r.chainKey.toLowerCase())?.key ?? r.chainKey;
1784
+ const all = Object.values(listed.crownsByChain ?? {}).flatMap((rows) => rows ?? []);
1785
+ const rows = wanted
1786
+ ? all.filter((r) => [keyOf(r), r.chainKey, r.chainName, names.get(r.chainKey.toLowerCase())?.legacyId ?? ""].some((v) => v.toLowerCase() === wanted))
1787
+ : all;
1788
+ const list = (rs) => rs.map((r) => `${keyOf(r)} #${r.tokenId}`).join(", ");
1789
+ if (rows.length === 1)
1790
+ return { chainKey: keyOf(rows[0]), crownId: rows[0].tokenId };
1791
+ if (all.length === 0)
1792
+ throw new IapError("PAGE_NOT_FOUND", `there is no token page for ${ref.symbol}`, 404);
1793
+ if (rows.length === 0)
1794
+ throw new IapError("PAGE_NOT_FOUND", `${ref.symbol} has no page on ${wanted}; its pages: ${list(all)}`, 404);
1795
+ throw new IapError("PAGE_AMBIGUOUS", `${ref.symbol} is ${rows.length} token pages (${list(rows)}); pass chain_key with crown_id, or the page's address with its chain`, 409);
1796
+ }
1797
+ /** chain_key + crown_id, or the page they are looked up from. */
1798
+ async function pageInput(a, required) {
1799
+ if (a.page) {
1800
+ if (a.crown_id !== undefined)
1801
+ throw new IapError("INVALID_PARAMS", "pass page, or chain_key with crown_id, not both", 400);
1802
+ return resolvePage(a.page, a.chain_key);
1803
+ }
1804
+ if (required && (!a.chain_key || a.crown_id === undefined)) {
1805
+ throw new IapError("INVALID_PARAMS", "pass page (the token page's symbol or address), or chain_key with crown_id", 400);
1806
+ }
1807
+ return { chainKey: a.chain_key, crownId: a.crown_id };
1808
+ }
1809
+ const iapPage = z.string().optional().describe("The token page, instead of chain_key and crown_id: its symbol ('FLUXGP') or its address ('chaindaddy.io/FLUXGP/base')");
1840
1810
  server.registerTool("iap_open_store", {
1841
1811
  title: "Open a creator store",
1842
- description: "Open (or re-open) the creator store for a token page. Idempotent per token page. Needs a Developer plan or Developer Beta membership, and the manager role on the token page.\n\nParams: chain_key (string, e.g. 'base'), crown_id (number, the token page's registration id), session_token?, idempotency_key?\nReturns: {ok, idempotency_key, store: {id, chainId, chainKey, crownId, symbol, tokenAddress, tokenDecimals, tokenOrigin, feeBps, payoutAddress, status, live, …}}\nErrors: NO_CREDENTIALS, NO_SESSION, NO_JWT, IAP_PLAN_REQUIRED, HTTP_403, HTTP_404",
1812
+ description: "Open (or re-open) the creator store for a token page. Idempotent per token page. Needs a Developer plan or Developer Beta membership, and the manager role on the token page.\n\nParams: page (the token page's symbol or address, e.g. 'FLUXGP' or 'chaindaddy.io/FLUXGP/base') OR chain_key (string, e.g. 'base') with crown_id (number, the token page's registration id); session_token?, idempotency_key?\nReturns: {ok, idempotency_key, store: {id, chainId, chainKey, crownId, symbol, tokenAddress, tokenDecimals, tokenOrigin, feeBps, payoutAddress, status, live, …}}\nErrors: NO_CREDENTIALS, NO_SESSION, NO_JWT, IAP_PLAN_REQUIRED, INVALID_PARAMS, PAGE_NOT_FOUND, PAGE_AMBIGUOUS (the symbol has several pages; the message lists them), HTTP_403, HTTP_404",
1843
1813
  inputSchema: {
1844
- chain_key: z.string().describe("Chain key of the token page, e.g. 'base', 'solana'"),
1845
- crown_id: z.number().int().nonnegative().describe("Registration id of the token page"),
1814
+ page: iapPage,
1815
+ chain_key: z.string().optional().describe("Chain key of the token page, e.g. 'base', 'solana' (with crown_id; or with page, to pick one of a symbol's pages)"),
1816
+ crown_id: z.number().int().nonnegative().optional().describe("Registration id of the token page (with chain_key)"),
1846
1817
  session_token: iapSessionToken,
1847
1818
  idempotency_key: iapIdempotencyKey,
1848
1819
  },
1849
1820
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1850
- }, async ({ chain_key, crown_id, session_token, idempotency_key }) => iapWrite("store", "POST", "/stores", { chainKey: chain_key, crownId: crown_id }, "account", session_token, idempotency_key));
1821
+ }, async ({ page, chain_key, crown_id, session_token, idempotency_key }) => {
1822
+ try {
1823
+ const at = await pageInput({ page, chain_key, crown_id }, true);
1824
+ return iapWrite("store", "POST", "/stores", at, "account", session_token, idempotency_key);
1825
+ }
1826
+ catch (error) {
1827
+ return iapFail(error);
1828
+ }
1829
+ });
1851
1830
  server.registerTool("iap_list_stores", {
1852
1831
  title: "List creator stores",
1853
- description: "List the creator stores you manage, optionally filtered to one token page.\n\nParams: chain_key?, crown_id?, session_token?\nReturns: {entries: Store[]}\nErrors: NO_CREDENTIALS, IAP_PLAN_REQUIRED",
1832
+ description: "List the creator stores you manage, optionally filtered to one token page. Each entry's id is the store_id the other iap_ tools take.\n\nParams: page? (the token page's symbol or address), chain_key?, crown_id?, session_token?\nReturns: {entries: Store[]}\nErrors: NO_CREDENTIALS, IAP_PLAN_REQUIRED, PAGE_NOT_FOUND, PAGE_AMBIGUOUS",
1854
1833
  inputSchema: {
1834
+ page: iapPage,
1855
1835
  chain_key: z.string().optional(),
1856
1836
  crown_id: z.number().int().nonnegative().optional(),
1857
1837
  session_token: iapSessionToken,
1858
1838
  },
1859
1839
  annotations: { readOnlyHint: true, openWorldHint: false },
1860
- }, async ({ chain_key, crown_id, session_token }) => iapRead(`/stores${iapQuery({ chainKey: chain_key, crownId: crown_id })}`, "account", session_token));
1840
+ }, async ({ page, chain_key, crown_id, session_token }) => {
1841
+ try {
1842
+ const at = await pageInput({ page, chain_key, crown_id }, false);
1843
+ return iapRead(`/stores${iapQuery({ chainKey: at.chainKey, crownId: at.crownId })}`, "account", session_token);
1844
+ }
1845
+ catch (error) {
1846
+ return iapFail(error);
1847
+ }
1848
+ });
1861
1849
  server.registerTool("iap_list_skus", {
1862
1850
  title: "List store items",
1863
1851
  description: "List a store's SKUs, including drafts and archived ones.\n\nParams: store_id, session_token?\nReturns: {entries: Sku[]} (price is base units of the SKU's currency — currency, currencyAddress, currencySymbol, currencyDecimals; null = not for sale)\nErrors: NO_CREDENTIALS, IAP_PLAN_REQUIRED, HTTP_404",
@@ -1866,7 +1854,7 @@ server.registerTool("iap_list_skus", {
1866
1854
  }, async ({ store_id, session_token }) => iapRead(`/stores/${enc(store_id)}/skus`, "store", session_token));
1867
1855
  server.registerTool("iap_create_sku", {
1868
1856
  title: "Create a store item",
1869
- description: "Create a SKU. Types: consumable (a balance: coins, boosts), non_consumable (owned once: skins, cars), timed (expiry extended by durationSeconds × quantity: season pass), unlock (never sold; held while `rule` holds). No price = reward-only. Price in base units (`price`) or in dollars (`priceUsdMicros`, with optional per-unit `minPrice`/`maxPrice` bounds in base units), never both. `currency` is what the buyer pays in: 'token' (default; recommended with priceUsdMicros — a steady dollar price, paid in the creator's token), 'native' or 'usdc'. GET /api/v2/iap/config lists each chain's currencies. `grants` makes a bundle. `splits` pays up to 2 more wallets (3 on a launchpad token) a share of each sale. New SKUs are drafts unless status is 'active'. Store key scope: catalog:write.\n\nParams: store_id, sku ({sku, type, name, currency?, price?, priceUsdMicros?, minPrice?, maxPrice?, grants?, splits?, durationSeconds?, rule?, maxSupply?, maxPerWallet?, saleStartsAt?, saleEndsAt?, status?, metadata?, description?, imageUrl?, sortOrder?}), session_token?, idempotency_key?\nReturns: {ok, idempotency_key, sku: Sku}\nErrors: NO_CREDENTIALS, IAP_PLAN_REQUIRED, HTTP_400 (validation), HTTP_409 (sku key taken)",
1857
+ description: "Create a SKU. Types: consumable (a balance: coins, boosts), non_consumable (owned once: skins, cars), timed (expiry extended by durationSeconds × quantity: season pass), unlock (never sold; held while `rule` holds). No price = reward-only. Price in base units (`price`) or in dollars (`priceUsdMicros`, with optional per-unit `minPrice`/`maxPrice` bounds in base units), never both. `currency` is what the buyer pays in: 'token' (default; recommended with priceUsdMicros — a steady dollar price, paid in the creator's token), 'native' or 'usdc'. GET /api/v2/iap/config lists each chain's currencies. `grants` makes a bundle. `splits` pays up to 2 more wallets (3 on a launchpad token) a share of each sale. New SKUs are drafts unless status is 'active'. Store key scope: catalog:write.\n\nParams: store_id, sku ({sku, type, name, currency?, price?, priceUsdMicros?, minPrice?, maxPrice?, grants?, splits?, durationSeconds?, rule?, maxSupply?, maxPerWallet?, maxAppSpend?, saleStartsAt?, saleEndsAt?, status?, metadata?, description?, imageUrl?, sortOrder?}), session_token?, idempotency_key?\nReturns: {ok, idempotency_key, sku: Sku}\nErrors: NO_CREDENTIALS, IAP_PLAN_REQUIRED, HTTP_400 (validation), HTTP_409 (sku key taken)",
1870
1858
  inputSchema: { store_id: iapStoreId, sku: iapSkuInput, session_token: iapSessionToken, idempotency_key: iapIdempotencyKey },
1871
1859
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1872
1860
  }, async ({ store_id, sku, session_token, idempotency_key }) => iapWrite("sku", "POST", `/stores/${enc(store_id)}/skus`, sku, "store", session_token, idempotency_key));
@@ -2148,6 +2136,48 @@ server.resource("chaindaddy://tools", "How the tools fit together: sign-in, crow
2148
2136
  }],
2149
2137
  }));
2150
2138
  // ============================================================================
2139
+ // SKILLS
2140
+ // ============================================================================
2141
+ // Chain Daddy's Agent Skills (the package's skills/ directory, the same files
2142
+ // the remote server embeds and the plugin copies) are served as prompts, one
2143
+ // per skill, and through two read-only tools for clients that only call tools.
2144
+ const SKILLS = loadSkills();
2145
+ for (const skill of SKILLS) {
2146
+ server.registerPrompt(skill.name, { title: skill.title, description: skill.description }, () => ({
2147
+ description: skill.description,
2148
+ messages: [{ role: "user", content: { type: "text", text: skill.body } }],
2149
+ }));
2150
+ }
2151
+ server.registerTool("list_skills", {
2152
+ title: "List the skills",
2153
+ description: "List Chain Daddy's skills: step-by-step workflows for launching or finding a token, checking its health, exporting a listing, selling items for it, signing users in, verifying receipts and building Crown Apps. Read one with read_skill before doing that task. The same skills are this server's prompts.",
2154
+ inputSchema: {},
2155
+ outputSchema: { skills: z.array(z.object({ name: z.string(), title: z.string(), description: z.string() })) },
2156
+ annotations: { readOnlyHint: true, openWorldHint: false },
2157
+ }, async () => ({
2158
+ content: [{
2159
+ type: "text",
2160
+ text: "Chain Daddy skills (read one with read_skill):\n" +
2161
+ SKILLS.map((s) => `\n- ${s.name} (${s.title}): ${s.description}`).join(""),
2162
+ }],
2163
+ structuredContent: { skills: SKILLS.map(({ name, title, description }) => ({ name, title, description })) },
2164
+ }));
2165
+ server.registerTool("read_skill", {
2166
+ title: "Read a skill",
2167
+ description: "Read one Chain Daddy skill in full, as Markdown: the workflow, the tools it uses and the rules it keeps. Pass a name from list_skills, and follow it for that task.",
2168
+ inputSchema: { name: z.string().describe("A skill name from list_skills.") },
2169
+ annotations: { readOnlyHint: true, openWorldHint: false },
2170
+ }, async ({ name }) => {
2171
+ const skill = SKILLS.find((s) => s.name === name.trim());
2172
+ if (!skill) {
2173
+ return {
2174
+ content: [{ type: "text", text: `no skill is named ${JSON.stringify(name)}; the skills are: ${SKILLS.map((s) => s.name).join(", ")}` }],
2175
+ isError: true,
2176
+ };
2177
+ }
2178
+ return { content: [{ type: "text", text: skill.body }] };
2179
+ });
2180
+ // ============================================================================
2151
2181
  // START SERVER
2152
2182
  // ============================================================================
2153
2183
  async function main() {