@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 +14 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +79 -16
- package/build/index.js.map +1 -1
- package/build/login.d.ts +11 -0
- package/build/login.d.ts.map +1 -0
- package/build/login.js +203 -0
- package/build/login.js.map +1 -0
- package/build/skills.d.ts +10 -0
- package/build/skills.d.ts.map +1 -0
- package/build/skills.js +46 -0
- package/build/skills.js.map +1 -0
- package/package.json +2 -1
- package/skills/add-crypto-to-your-game/SKILL.md +114 -0
- package/skills/build-a-crown-app/SKILL.md +123 -0
- package/skills/check-token-health/SKILL.md +67 -0
- package/skills/export-a-listing/SKILL.md +51 -0
- package/skills/find-and-check-a-token/SKILL.md +86 -0
- package/skills/launch-a-token/SKILL.md +102 -0
- package/skills/sell-items-in-your-app/SKILL.md +108 -0
- package/skills/sign-in-with-chain-daddy/SKILL.md +116 -0
- package/skills/verify-receipts-and-sessions/SKILL.md +106 -0
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/
|
|
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
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;
|
|
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
|
|
68
|
-
"
|
|
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
|
|
75
|
-
//
|
|
76
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() {
|