@chaindaddy/mcp 0.4.2 → 0.6.0

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
@@ -57,11 +57,14 @@ Free key is enough. Connect your wallet at [chaindaddy.io/_api](https://chaindad
57
57
  a tool says so and stops before calling the API. Where the client passes your shell's environment to MCP
58
58
  servers (Claude Code does), export it in your shell instead of writing it into a file.
59
59
 
60
+ No key at all: run `npx @chaindaddy/cli login` once on the same computer. The server uses that login (and keeps
61
+ it refreshed) whenever `CHAINDADDY_API_KEY` is unset.
62
+
60
63
  ### Environment Variables
61
64
 
62
65
  | Variable | Default | Description |
63
66
  |----------|---------|-------------|
64
- | `CHAINDADDY_API_KEY` | — | Your `cd_live_` API key. Sent as `X-API-Key` on every call, and the Bearer credential for creator and admin calls when a tool gets no `session_token` |
67
+ | `CHAINDADDY_API_KEY` | — | Your `cd_live_` API key. Sent as `X-API-Key` on every call, and the Bearer credential for creator and admin calls when a tool gets no `session_token`. Unset, the CLI's `chaindaddy login` is used when there is one |
65
68
  | `CHAINDADDY_IAP_KEY` | — | A `cd_iap_` store key for your app's server: one store, explicit scopes. Used by the store-scoped `iap_*` tools when no `session_token` is passed |
66
69
  | `CHAINDADDY_API_URL` | `https://api.chaindaddy.io` | Chain Daddy API endpoint. A `localhost` API needs no key |
67
70
  | `CHAINDADDY_NETWORK` | `mainnet` | A label shown in `chaindaddy://config`; it does not change which API is called |
@@ -140,7 +143,7 @@ or Developer Beta membership on the wallet that opens the store (otherwise `403
140
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 |
141
144
 
142
145
  Every write sends an `Idempotency-Key` and returns it as `idempotency_key`; retry with the same key and the
143
- 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).
144
147
 
145
148
  ```
146
149
  "Open a store for my token on Base and add a gold badge for $5"
@@ -148,6 +151,15 @@ write never applies twice. The orchestration skill is [`skills/creator-store.md`
148
151
  "Give every wallet in this list 10 credits"
149
152
  ```
150
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
+
151
163
  ## Example Conversations
152
164
 
153
165
  ### Search and Discover
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;AAgDH,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
@@ -14,6 +14,8 @@ import { createRequire } from "node:module";
14
14
  import { SiweMessage } from "siwe";
15
15
  import nacl from "tweetnacl";
16
16
  import bs58 from "bs58";
17
+ import { CLI_LOGIN, hasCliLogin, loginFetch } from "./login.js";
18
+ import { loadSkills } from "./skills.js";
17
19
  // Configuration
18
20
  const API_URL = process.env.CHAINDADDY_API_URL || "https://api.chaindaddy.io";
19
21
  // A recommender service you run yourself. The hosted API has none, so with
@@ -64,22 +66,26 @@ export function requireSessionJWT(session_token) {
64
66
  }
65
67
  return { session, jwt: session.jwt };
66
68
  }
67
- const NO_API_KEY_MESSAGE = "NO_API_KEY: CHAINDADDY_API_KEY is not set, and the Chain Daddy API answers callers outside chaindaddy.io " +
68
- "only with an API key. Get a free one: connect your wallet at https://chaindaddy.io/_api and click " +
69
+ const NO_API_KEY_MESSAGE = "NO_API_KEY: CHAINDADDY_API_KEY is not set and this computer is not signed in, and the Chain Daddy API " +
70
+ "answers callers outside chaindaddy.io only with either. Easiest: run `npx @chaindaddy/cli login` once " +
71
+ "on this computer, then restart this server. Or get a free key: connect your wallet at https://chaindaddy.io/_api and click " +
69
72
  "Generate API Key (a developer-program member can also run `chaindaddy app key create`). Then set " +
70
73
  "CHAINDADDY_API_KEY=cd_live_… where this server starts. For read-only search, token and health lookups " +
71
74
  "with no key, connect the remote server https://api.chaindaddy.io/mcp instead.";
72
75
  // Paths the API serves with no key (pkg/middleware/origin.go PublicPaths).
73
76
  const KEYLESS_PATHS = ["/api/v2/tokenlist", "/api/v2/iap", "/api/v2/heartbeat/oracle"];
74
- // Headers for a call to the API: X-API-Key when it is set. With no key, a call
75
- // that needs one fails here with what to do, rather than as a bare 401. A local
76
- // API (dev mode, localhost) needs no key.
77
+ // Headers for a call to the API: X-API-Key when it is set, else the CLI's
78
+ // stored `chaindaddy login` (the bearer marker loginFetch swaps for DPoP). With
79
+ // neither, a call that needs one fails here with what to do, rather than as a
80
+ // bare 401. A local API (dev mode, localhost) needs no key.
77
81
  function apiHeaders(endpoint, baseUrl = API_URL) {
78
82
  const headers = {};
79
83
  if (API_KEY) {
80
84
  headers["X-API-Key"] = API_KEY;
81
85
  return headers;
82
86
  }
87
+ if (hasCliLogin(baseUrl))
88
+ return { Authorization: `Bearer ${CLI_LOGIN}` };
83
89
  const path = endpoint.split("?")[0];
84
90
  const keyless = KEYLESS_PATHS.some((p) => path === p || path.startsWith(`${p}/`));
85
91
  const local = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(baseUrl);
@@ -101,7 +107,7 @@ async function apiCall(endpoint, options = {}) {
101
107
  Object.assign(headers, options.headers);
102
108
  if (options.jwt)
103
109
  headers["Authorization"] = `Bearer ${options.jwt}`;
104
- const response = await fetch(url, { headers });
110
+ const response = await loginFetch(API_URL, url, { headers });
105
111
  if (!response.ok)
106
112
  throw await apiError(response);
107
113
  return response.json();
@@ -116,7 +122,7 @@ async function apiPost(endpoint, body, headers = {}, options = {}) {
116
122
  };
117
123
  if (options.jwt)
118
124
  finalHeaders["Authorization"] = `Bearer ${options.jwt}`;
119
- const response = await fetch(url, {
125
+ const response = await loginFetch(API_URL, url, {
120
126
  method: "POST",
121
127
  headers: finalHeaders,
122
128
  body: JSON.stringify(body),
@@ -129,7 +135,7 @@ async function apiPost(endpoint, body, headers = {}, options = {}) {
129
135
  // so it's a mandatory option rather than `optional: jwt?`.
130
136
  async function apiPut(endpoint, body, options) {
131
137
  const url = `${API_URL}${endpoint}`;
132
- const response = await fetch(url, {
138
+ const response = await loginFetch(API_URL, url, {
133
139
  method: "PUT",
134
140
  headers: {
135
141
  ...apiHeaders(endpoint),
@@ -145,7 +151,7 @@ async function apiPut(endpoint, body, options) {
145
151
  // Helper for DELETE requests
146
152
  async function apiDelete(endpoint, headers = {}) {
147
153
  const url = `${API_URL}${endpoint}`;
148
- const response = await fetch(url, {
154
+ const response = await loginFetch(API_URL, url, {
149
155
  method: "DELETE",
150
156
  headers: {
151
157
  ...apiHeaders(endpoint),
@@ -184,9 +190,12 @@ function iapBearer(auth, session_token) {
184
190
  return IAP_KEY;
185
191
  if (API_KEY)
186
192
  return API_KEY;
193
+ // No key: the Chain Daddy CLI's `chaindaddy login`, when it is stored here.
194
+ if (hasCliLogin(API_URL))
195
+ return CLI_LOGIN;
187
196
  throw new IapError("NO_CREDENTIALS", auth === "store"
188
- ? "Pass session_token, or set CHAINDADDY_IAP_KEY (a store key) or CHAINDADDY_API_KEY (a cd_live_ key)"
189
- : "Pass session_token, or set CHAINDADDY_API_KEY (a cd_live_ key)", 401);
197
+ ? "Pass session_token, run `chaindaddy login`, or set CHAINDADDY_IAP_KEY (a store key) or CHAINDADDY_API_KEY (a cd_live_ key)"
198
+ : "Pass session_token, run `chaindaddy login`, or set CHAINDADDY_API_KEY (a cd_live_ key)", 401);
190
199
  }
191
200
  async function iapRequest(method, path, opts) {
192
201
  const headers = { Accept: "application/json" };
@@ -204,7 +213,7 @@ async function iapRequest(method, path, opts) {
204
213
  }
205
214
  if (opts.body !== undefined)
206
215
  headers["Content-Type"] = "application/json";
207
- const response = await fetch(`${API_URL}/api/v2/iap${path}`, {
216
+ const response = await loginFetch(API_URL, `${API_URL}/api/v2/iap${path}`, {
208
217
  method,
209
218
  headers,
210
219
  body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
@@ -269,6 +278,10 @@ const VERSION = createRequire(import.meta.url)("../package.json").version;
269
278
  const server = new McpServer({
270
279
  name: "chaindaddy",
271
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.",
272
285
  });
273
286
  // ============================================================================
274
287
  // TOOLS
@@ -1105,9 +1118,10 @@ server.registerTool("get_recommendations", {
1105
1118
  // value, so we only include keys the caller actually passed.
1106
1119
  server.registerTool("update_profile", {
1107
1120
  title: "Update token page profile",
1108
- description: "Update crown profile metadata via PUT /api/v2/registration/{id}/profile. Requires a session bound to a backend JWT (run authenticate first) and the session wallet must own or manage the crown.\n\nParams: crown_id (string), description (string?), tags (string[]?), category ('meme'|'gaming'|'defi'|'nft'|'social'|'utility'?) [NOT FORWARDED — no backend route exposes category on the profile endpoint], website (string?), socials ({twitter?, telegram?, discord?}?), logo_url (string?), session_token (string)\nReturns: {updated_fields: string[]}\nErrors: NO_SESSION, SESSION_EXPIRED, NO_JWT, NOT_OWNER, NO_FIELDS, API_ERROR",
1121
+ description: "Update crown profile metadata via PUT /api/v2/registration/{id}/profile?chain={chain}. Requires a session bound to a backend JWT (run authenticate first) and the session wallet must own or manage the crown on that chain.\n\nParams: crown_id (string), chain (string; required, a token id names a different crown on every chain), description (string?), tags (string[]?), category ('meme'|'gaming'|'defi'|'nft'|'social'|'utility'?) [NOT FORWARDED — no backend route exposes category on the profile endpoint], website (string?), socials ({twitter?, telegram?, discord?}?), logo_url (string?), session_token (string)\nReturns: {updated_fields: string[]}\nErrors: NO_SESSION, SESSION_EXPIRED, NO_JWT, NOT_OWNER, NO_FIELDS, API_ERROR",
1109
1122
  inputSchema: {
1110
1123
  crown_id: z.string().describe("Crown ID (token ID)"),
1124
+ chain: z.string().describe("Chain of the crown (e.g. 'base', 'arbitrum', 'solana'). Required: a token id names a different crown on every chain, and the role check and write happen on this one."),
1111
1125
  description: z.string().optional().describe("Token/project description"),
1112
1126
  tags: z.array(z.string()).optional().describe("Descriptive tags (e.g., ['meme', 'dog', 'community'])"),
1113
1127
  category: z.enum(["meme", "gaming", "defi", "nft", "social", "utility"]).optional().describe("Project category. ACCEPTED but NOT forwarded — the profile endpoint does not expose this field today; the input is preserved here for API stability."),
@@ -1121,7 +1135,7 @@ server.registerTool("update_profile", {
1121
1135
  session_token: z.string().describe("Session token from authenticate (must have been issued post-JWT-bridge, i.e. after fix/mcp-siwe-jwt-bridge)"),
1122
1136
  },
1123
1137
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
1124
- }, async ({ crown_id, description, tags, category, website, socials, logo_url, session_token }) => {
1138
+ }, async ({ crown_id, chain, description, tags, category, website, socials, logo_url, session_token }) => {
1125
1139
  void category;
1126
1140
  try {
1127
1141
  const { jwt } = requireSessionJWT(session_token);
@@ -1151,7 +1165,7 @@ server.registerTool("update_profile", {
1151
1165
  isError: true,
1152
1166
  };
1153
1167
  }
1154
- await apiPut(`/api/v2/registration/${encodeURIComponent(crown_id)}/profile`, payload, { jwt });
1168
+ await apiPut(`/api/v2/registration/${encodeURIComponent(crown_id)}/profile?chain=${encodeURIComponent(chain)}`, payload, { jwt });
1155
1169
  return {
1156
1170
  content: [{ type: "text", text: JSON.stringify({
1157
1171
  updated_fields: Object.keys(payload),
@@ -1743,6 +1757,13 @@ const iapSkuFields = {
1743
1757
  rule: iapRule.nullable(),
1744
1758
  maxSupply: z.number().int().positive().nullable(),
1745
1759
  maxPerWallet: z.number().int().positive().nullable(),
1760
+ maxAppSpend: z
1761
+ .number()
1762
+ .int()
1763
+ .positive()
1764
+ .max(1_000_000)
1765
+ .nullable()
1766
+ .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"),
1746
1767
  saleStartsAt: z.string().nullable(),
1747
1768
  saleEndsAt: z.string().nullable(),
1748
1769
  status: z.enum(["draft", "active", "archived"]),
@@ -1857,7 +1878,7 @@ server.registerTool("iap_list_skus", {
1857
1878
  }, async ({ store_id, session_token }) => iapRead(`/stores/${enc(store_id)}/skus`, "store", session_token));
1858
1879
  server.registerTool("iap_create_sku", {
1859
1880
  title: "Create a store item",
1860
- 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)",
1881
+ 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)",
1861
1882
  inputSchema: { store_id: iapStoreId, sku: iapSkuInput, session_token: iapSessionToken, idempotency_key: iapIdempotencyKey },
1862
1883
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1863
1884
  }, async ({ store_id, sku, session_token, idempotency_key }) => iapWrite("sku", "POST", `/stores/${enc(store_id)}/skus`, sku, "store", session_token, idempotency_key));
@@ -2139,6 +2160,48 @@ server.resource("chaindaddy://tools", "How the tools fit together: sign-in, crow
2139
2160
  }],
2140
2161
  }));
2141
2162
  // ============================================================================
2163
+ // SKILLS
2164
+ // ============================================================================
2165
+ // Chain Daddy's Agent Skills (the package's skills/ directory, the same files
2166
+ // the remote server embeds and the plugin copies) are served as prompts, one
2167
+ // per skill, and through two read-only tools for clients that only call tools.
2168
+ const SKILLS = loadSkills();
2169
+ for (const skill of SKILLS) {
2170
+ server.registerPrompt(skill.name, { title: skill.title, description: skill.description }, () => ({
2171
+ description: skill.description,
2172
+ messages: [{ role: "user", content: { type: "text", text: skill.body } }],
2173
+ }));
2174
+ }
2175
+ server.registerTool("list_skills", {
2176
+ title: "List the skills",
2177
+ 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.",
2178
+ inputSchema: {},
2179
+ outputSchema: { skills: z.array(z.object({ name: z.string(), title: z.string(), description: z.string() })) },
2180
+ annotations: { readOnlyHint: true, openWorldHint: false },
2181
+ }, async () => ({
2182
+ content: [{
2183
+ type: "text",
2184
+ text: "Chain Daddy skills (read one with read_skill):\n" +
2185
+ SKILLS.map((s) => `\n- ${s.name} (${s.title}): ${s.description}`).join(""),
2186
+ }],
2187
+ structuredContent: { skills: SKILLS.map(({ name, title, description }) => ({ name, title, description })) },
2188
+ }));
2189
+ server.registerTool("read_skill", {
2190
+ title: "Read a skill",
2191
+ 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.",
2192
+ inputSchema: { name: z.string().describe("A skill name from list_skills.") },
2193
+ annotations: { readOnlyHint: true, openWorldHint: false },
2194
+ }, async ({ name }) => {
2195
+ const skill = SKILLS.find((s) => s.name === name.trim());
2196
+ if (!skill) {
2197
+ return {
2198
+ content: [{ type: "text", text: `no skill is named ${JSON.stringify(name)}; the skills are: ${SKILLS.map((s) => s.name).join(", ")}` }],
2199
+ isError: true,
2200
+ };
2201
+ }
2202
+ return { content: [{ type: "text", text: skill.body }] };
2203
+ });
2204
+ // ============================================================================
2142
2205
  // START SERVER
2143
2206
  // ============================================================================
2144
2207
  async function main() {