phygital-token-mcp 0.2.2 → 0.4.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/dist/index.js CHANGED
@@ -1,488 +1,190 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { findAssetPda, formatGatingPredicate, parseSecp256r1Pubkey, summarizeGatingEvaluationFailure, summarizeGatingFailure, } from "phygital-token-sdk";
4
+ import { findAssetPda, parseSecp256r1Pubkey } from "phygital-token-sdk";
5
5
  import { z } from "zod";
6
6
  import { listDocs, readDocById, searchDocs } from "./lib/docs.js";
7
7
  import { jsonResult, textResult } from "./lib/format.js";
8
- import { GATING_FILTER_SCHEMA, GATING_OVERVIEW, GATING_RECIPES, GATING_TIER_EXAMPLE, getGatingRecipe, } from "./lib/gating-json.js";
9
- import { runGraphify } from "./lib/graphify.js";
10
8
  import { parseAssetType, planCreateMint, planMintToken, planRemoveOwnership, planTransfer, planVerifyAsset, } from "./lib/instructions.js";
11
- import { SDK_SURFACE, readSourceExcerpt } from "./lib/sdk-surface.js";
12
- import { ON_CHAIN_PATTERNS, VERIFICATION_DECISION_TREE, listVerificationUseCases, recommendVerification, } from "./lib/verification.js";
9
+ import { SDK_SURFACE } from "./lib/sdk-surface.js";
10
+ import { VERIFICATION_DECISION_TREE, listVerificationUseCases, recommendVerification, } from "./lib/verification.js";
11
+ const VERSION = "0.4.0";
13
12
  const SERVER_INSTRUCTIONS = [
14
13
  "MCP server for the phygital-token Solana program, TypeScript SDK, and Rust client.",
15
14
  "Docs, schema reference, and offline planning only — no live on-chain RPC calls.",
16
15
  "",
17
16
  "Routing:",
18
- "- Which verification to use → recommend_verification or explain_verification",
19
- "- Composable verify_asset plan_verify_asset, read_doc verify-asset-composable",
20
- "- Building on phygital (CPI) → read_doc building-on-phygital/*, rust_cpi_verify_asset_guide",
21
- "- SDK export map → list_sdk_exports",
22
- "- Gating rules → explain_gating, gating_filter_schema, gating_recipe, format_gating_predicate",
17
+ "- Which verification method to use → recommend_verification",
18
+ "- On-chain verify_asset (standalone, sysvar inspect, or CPI) → plan_verify_asset",
23
19
  "- Mint / transfer / forfeiture flows → plan_create_mint, plan_mint_token, plan_transfer, plan_remove_ownership",
24
- "- Code architecture (repo clone) query_codebase (graphify)",
20
+ "- SDK export maplist_sdk_exports",
21
+ "- Anything else → search_docs, then read_doc",
25
22
  "",
26
- "Live gating evaluation and asset fetch: use phygital-token-sdk in your app (evaluateAssetGating, fetchAssetDisplayInfo).",
27
- "query_codebase and read_sdk_source need PHYGITAL_TOKEN_REPO_ROOT when not running from a clone.",
23
+ "Live asset fetch and auth: call phygital-token-sdk directly in your app",
24
+ "(verifyResponse, fetchAssetDisplayInfo, etc.).",
28
25
  ].join("\n");
26
+ /** Every tool here is offline and side-effect free. */
27
+ const READ_ONLY = {
28
+ readOnlyHint: true,
29
+ destructiveHint: false,
30
+ idempotentHint: true,
31
+ openWorldHint: false,
32
+ };
29
33
  function registerTools(server) {
30
- server.tool("search_docs", "Search phygital-token docs: gating, verification, building-on-phygital, SDK surface, glossary.", {
31
- query: z.string().describe("Natural-language or keyword search query"),
32
- limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8)"),
34
+ server.registerTool("search_docs", {
35
+ description: "Search phygital-token docs (verification, building-on-phygital, SDK surface, glossary). Omit query to list every doc id.",
36
+ inputSchema: {
37
+ query: z
38
+ .string()
39
+ .optional()
40
+ .describe("Keyword or natural-language query. Omit to list all docs."),
41
+ limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8)"),
42
+ },
43
+ annotations: { title: "Search docs", ...READ_ONLY },
33
44
  }, async ({ query, limit }) => {
34
- const results = await searchDocs(query, limit ?? 8);
35
- return jsonResult({ query, results });
36
- });
37
- server.tool("read_doc", "Read a full documentation file by id from search_docs results.", {
38
- docId: z.string().describe('Document id, e.g. "gating:overview" or "verification:overview"'),
39
- }, async ({ docId }) => textResult(await readDocById(docId)));
40
- server.tool("list_docs", "List all available phygital-token documentation resources.", {}, async () => jsonResult({ docs: await listDocs() }));
41
- server.tool("query_codebase", "Query the phygital-token knowledge graph via graphify (architecture, call paths, concepts). Requires a repo clone.", {
42
- question: z.string().describe("Natural-language question about the codebase"),
43
- mode: z
44
- .enum(["query", "explain", "path"])
45
- .optional()
46
- .describe("query=BFS context (default), explain=concept summary, path=shortest path between two nodes"),
47
- from: z.string().optional().describe('Required for mode=path: start concept/node name'),
48
- to: z.string().optional().describe('Required for mode=path: end concept/node name'),
49
- }, async ({ question, mode = "query", from, to }) => {
50
- if (mode === "path") {
51
- if (!from || !to) {
52
- throw new Error('mode "path" requires both from and to.');
53
- }
54
- const output = await runGraphify("path", [from, to]);
55
- return textResult(output);
56
- }
57
- if (mode === "explain") {
58
- const output = await runGraphify("explain", [question]);
59
- return textResult(output);
60
- }
61
- const output = await runGraphify("query", [question]);
62
- return textResult(output);
63
- });
64
- server.tool("find_asset_pda", "Derive the on-chain asset PDA address from a secp256r1 passkey public key (offline).", {
65
- assetPublicKey: z
66
- .string()
67
- .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
68
- }, async ({ assetPublicKey }) => {
69
- const pubkey = parseSecp256r1Pubkey(assetPublicKey);
70
- const assetPda = await findAssetPda(pubkey);
71
- return jsonResult({ assetPublicKey, assetPda });
72
- });
73
- server.tool("gating_tier_example", "Return a copy-paste JSON example for evaluateAssetGating tier configuration.", {}, async () => jsonResult({ tiers: GATING_TIER_EXAMPLE }));
74
- server.tool("explain_gating", "Gating mental model: dimensions, flow, aggregations, same-asset vs wallet pitfalls, doc index.", {}, async () => jsonResult(GATING_OVERVIEW));
75
- server.tool("gating_filter_schema", "JSON schema reference for GatingFilter trees, predicates, traits, and SDK builders.", {}, async () => jsonResult(GATING_FILTER_SCHEMA));
76
- server.tool("list_gating_recipes", "List copy-paste gating recipes (filters, tiers, and common footguns).", {
77
- includeFootguns: z
78
- .boolean()
79
- .optional()
80
- .describe("Include anti-pattern / footgun recipes (default true)"),
81
- }, async ({ includeFootguns = true }) => jsonResult({
82
- recipes: GATING_RECIPES.filter((r) => includeFootguns || !r.footgun).map(({ id, title, description, footgun }) => ({
83
- id,
84
- title,
85
- description,
86
- footgun: footgun ?? false,
87
- })),
88
- }));
89
- server.tool("gating_recipe", "Return a gating recipe by id (JSON filter or tiers for evaluateAssetGating).", {
90
- recipeId: z.string().describe("Recipe id from list_gating_recipes"),
91
- }, async ({ recipeId }) => jsonResult(getGatingRecipe(recipeId)));
92
- server.tool("summarize_gating_result", "Human-readable failure reasons from evaluateAssetGating output (pass the SDK result JSON).", {
93
- evaluationJson: z.unknown().describe("JSON result from evaluateAssetGating"),
94
- }, async ({ evaluationJson }) => {
95
- const data = evaluationJson;
96
- if (data.failureSummary && Array.isArray(data.failureSummary)) {
97
- return jsonResult({ summary: data.failureSummary });
98
- }
99
- if (data.tiers && Array.isArray(data.tiers)) {
100
- const summary = summarizeGatingEvaluationFailure(data);
101
- return jsonResult({ summary });
102
- }
103
- if (data.filterResult) {
45
+ if (!query?.trim()) {
46
+ const docs = await listDocs();
104
47
  return jsonResult({
105
- summary: summarizeGatingFailure(data.filterResult),
48
+ docs: docs.map(({ id, title, category }) => ({ id, title, category })),
106
49
  });
107
50
  }
108
- throw new Error("Unrecognized evaluation JSON. Pass output from evaluateAssetGating in your app.");
109
- });
110
- server.tool("format_gating_predicate", "Format a GatingAssetPredicate JSON object as a human-readable string.", {
111
- predicate: z.unknown().describe("GatingAssetPredicate JSON"),
112
- }, async ({ predicate }) => {
113
- if (!predicate || typeof predicate !== "object") {
114
- throw new Error("predicate must be a JSON object");
115
- }
116
- return jsonResult({
117
- formatted: formatGatingPredicate(predicate),
118
- });
119
- });
120
- server.tool("list_gating_docs", "List all gating documentation doc ids (overview, predicates, recipes, etc.).", {}, async () => {
121
- const docs = await listDocs();
122
- return jsonResult({
123
- docs: docs.filter((d) => d.category === "gating"),
124
- });
51
+ return jsonResult({ query, results: await searchDocs(query, limit ?? 8) });
125
52
  });
126
- server.tool("plan_create_mint", "Validate metadata and return accounts/signers needed for create_mint (buildCreateMintInstructions).", {
127
- name: z.string().describe("Token metadata name (max 32 chars)"),
128
- symbol: z.string().describe("Token metadata symbol (max 10 chars)"),
129
- uri: z.string().describe("Metadata URI (max 200 chars)"),
53
+ server.registerTool("read_doc", {
54
+ description: "Read a full documentation file by id from search_docs results.",
55
+ inputSchema: {
56
+ docId: z
57
+ .string()
58
+ .describe('Document id, e.g. "verification:methods" or "sdk:surface-area"'),
59
+ },
60
+ annotations: { title: "Read doc", ...READ_ONLY },
61
+ }, async ({ docId }) => textResult(await readDocById(docId)));
62
+ server.registerTool("recommend_verification", {
63
+ description: "Pick the right SDK verification method (identification vs authentication, off-chain vs on-chain). Omit useCase to get the decision tree and all use cases.",
64
+ inputSchema: {
65
+ useCase: z
66
+ .enum([
67
+ "product_page_lookup",
68
+ "deep_link_from_prior_scan",
69
+ "offline_identification",
70
+ "login_ui_only",
71
+ "onchain_standalone_verify",
72
+ "onchain_inspect_verify_asset",
73
+ "onchain_cpi_verify_asset",
74
+ "transfer_ownership",
75
+ "native_mobile_app",
76
+ ])
77
+ .optional()
78
+ .describe("Omit to list all use cases with the decision tree."),
79
+ },
80
+ annotations: { title: "Recommend verification", ...READ_ONLY },
81
+ }, async ({ useCase }) => jsonResult(useCase
82
+ ? recommendVerification(useCase)
83
+ : {
84
+ decisionTree: VERIFICATION_DECISION_TREE,
85
+ useCases: listVerificationUseCases(),
86
+ }));
87
+ server.registerTool("plan_verify_asset", {
88
+ description: "Plan the on-chain verify_asset flow (offline): transaction layout, derived accounts, message binding. standalone = verify_asset only; inspect = Pattern A (your program reads the instructions sysvar); cpi = Pattern B (your program CPIs verify_asset).",
89
+ inputSchema: {
90
+ message: z
91
+ .string()
92
+ .describe("UTF-8 message bytes bound into the on-chain proof (embed your domain-specific payload)"),
93
+ onChainPattern: z
94
+ .enum(["inspect", "cpi", "standalone"])
95
+ .optional()
96
+ .describe("Default inspect (Pattern A)."),
97
+ assetPublicKey: z
98
+ .string()
99
+ .optional()
100
+ .describe("Base64url secp256r1 pubkey, to pre-derive the asset PDA"),
101
+ },
102
+ annotations: { title: "Plan verify_asset", ...READ_ONLY },
103
+ }, async ({ message, onChainPattern, assetPublicKey }) => jsonResult(await planVerifyAsset({
104
+ message,
105
+ assetPublicKey,
106
+ onChainPattern: onChainPattern ?? "inspect",
107
+ })));
108
+ server.registerTool("plan_create_mint", {
109
+ description: "Validate design metadata and return the accounts/signers for create_mint (buildCreateMintInstructions).",
110
+ inputSchema: {
111
+ name: z.string().describe("Token metadata name (max 32 chars)"),
112
+ symbol: z.string().describe("Token metadata symbol (max 10 chars)"),
113
+ uri: z.string().describe("Metadata URI (max 200 chars)"),
114
+ },
115
+ annotations: { title: "Plan create_mint", ...READ_ONLY },
130
116
  }, async ({ name, symbol, uri }) => jsonResult(planCreateMint({ name, symbol, uri })));
131
- server.tool("plan_mint_token", "Derive accounts and list signers/inputs for mint_token (buildMintTokenInstructions).", {
132
- assetPublicKey: z
133
- .string()
134
- .describe("Base64url-encoded secp256r1 public key for the new asset"),
135
- mint: z.string().describe("Design mint address"),
136
- assetType: z
137
- .enum(["Lockable", "Transferable"])
138
- .describe("Asset transfer lock behavior"),
139
- credentialId: z
140
- .string()
141
- .optional()
142
- .describe("Base64url passkey credential id (for reference in plan output)"),
117
+ server.registerTool("plan_mint_token", {
118
+ description: "Derive accounts and list signers/inputs for mint_token (buildMintTokenInstructions).",
119
+ inputSchema: {
120
+ assetPublicKey: z
121
+ .string()
122
+ .describe("Base64url-encoded secp256r1 public key for the new asset"),
123
+ mint: z.string().describe("Design mint address"),
124
+ assetType: z.enum(["Lockable", "Transferable"]).describe("Asset transfer lock behavior"),
125
+ credentialId: z
126
+ .string()
127
+ .optional()
128
+ .describe("Base64url passkey credential id (echoed in the plan output)"),
129
+ },
130
+ annotations: { title: "Plan mint_token", ...READ_ONLY },
143
131
  }, async ({ assetPublicKey, mint, assetType, credentialId }) => jsonResult(await planMintToken({
144
132
  assetPublicKey,
145
133
  mint,
146
134
  assetType: parseAssetType(assetType),
147
135
  credentialId,
148
136
  })));
149
- server.tool("plan_remove_ownership", "Plan a wallet-signed forfeiture: return token to custody and reset asset.owner (offline).", {
150
- assetPublicKey: z
151
- .string()
152
- .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
153
- owner: z
154
- .string()
155
- .describe("Current asset owner wallet — must match asset.owner on-chain"),
156
- mint: z.string().describe("Design mint address for the asset"),
157
- }, async ({ assetPublicKey, owner, mint }) => jsonResult(await planRemoveOwnership({ assetPublicKey, owner, mint })));
158
- server.tool("plan_transfer", "Plan a passkey-authorized transfer: flow steps, derived accounts, and challenge formula (offline).", {
159
- assetPublicKey: z
160
- .string()
161
- .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
162
- recipient: z.string().describe("Recipient wallet address"),
163
- mint: z
164
- .string()
165
- .optional()
166
- .describe("Design mint address — enables ATA derivation when combined with currentOwner"),
167
- currentOwner: z
168
- .string()
169
- .optional()
170
- .describe("Current asset owner wallet — enables ATA derivation when combined with mint"),
137
+ server.registerTool("plan_transfer", {
138
+ description: "Plan a passkey-authorized transfer (offline): flow steps, derived accounts, and challenge formula.",
139
+ inputSchema: {
140
+ assetPublicKey: z
141
+ .string()
142
+ .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
143
+ recipient: z.string().describe("Recipient wallet address"),
144
+ mint: z
145
+ .string()
146
+ .optional()
147
+ .describe("Design mint address — with currentOwner, enables ATA derivation"),
148
+ currentOwner: z
149
+ .string()
150
+ .optional()
151
+ .describe("Current asset owner wallet — with mint, enables ATA derivation"),
152
+ },
153
+ annotations: { title: "Plan transfer", ...READ_ONLY },
171
154
  }, async ({ assetPublicKey, recipient, mint, currentOwner }) => jsonResult(await planTransfer({ assetPublicKey, recipient, mint, currentOwner })));
172
- server.tool("recommend_verification", "Recommend which SDK verification method to use for a specific use case (identification vs authentication).", {
173
- useCase: z.enum([
174
- "product_page_lookup",
175
- "deep_link_from_prior_scan",
176
- "offline_identification",
177
- "login_ui_only",
178
- "vault_gated_experience",
179
- "onchain_standalone_verify",
180
- "onchain_inspect_verify_asset",
181
- "onchain_cpi_verify_asset",
182
- "transfer_ownership",
183
- "wallet_holdings_gate",
184
- "native_mobile_app",
185
- ]),
186
- }, async ({ useCase }) => jsonResult(recommendVerification(useCase)));
187
- server.tool("list_verification_use_cases", "List all supported verification use cases for recommend_verification.", {}, async () => jsonResult({
188
- useCases: listVerificationUseCases(),
189
- decisionTree: VERIFICATION_DECISION_TREE,
190
- }));
191
- server.tool("explain_verification", "Explain identification vs authentication and all verify.ts methods. Returns decision tree + doc pointers.", {}, async () => jsonResult({
192
- decisionTree: VERIFICATION_DECISION_TREE,
193
- useCases: listVerificationUseCases(),
194
- onChainPatterns: ON_CHAIN_PATTERNS,
195
- methods: {
196
- identification: ["verifyDynamicUrl", "verifyDynamicUrlWithoutCounterCheck"],
197
- authenticationOffChain: [
198
- "startAuthenticationWithChallengeResponse",
199
- "verifyWithChallengeResponse",
200
- ],
201
- onChainComposable: [
202
- "beginVerifyAsset",
203
- "buildVerifyAssetArgs",
204
- "completeVerifyAsset",
205
- ],
206
- transfer: ["beginTransfer", "completeTransfer"],
155
+ server.registerTool("plan_remove_ownership", {
156
+ description: "Plan a wallet-signed forfeiture (offline): return the token to custody and reset asset.owner.",
157
+ inputSchema: {
158
+ assetPublicKey: z
159
+ .string()
160
+ .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
161
+ owner: z.string().describe("Current asset owner wallet — must match asset.owner on-chain"),
162
+ mint: z.string().describe("Design mint address for the asset"),
207
163
  },
208
- docIds: [
209
- "verification:overview",
210
- "verification:methods",
211
- "verification:verify-asset-composable",
212
- "building-on-phygital:overview",
213
- ],
214
- }));
215
- server.tool("plan_verify_asset", "Plan on-chain verify_asset flow (offline). Pattern A: client posts verify_asset. Pattern B: program CPIs.", {
216
- message: z
217
- .string()
218
- .describe("UTF-8 message bytes bound into the on-chain proof (embed domain-specific payload for your program)"),
219
- onChainPattern: z
220
- .enum(["inspect", "cpi", "standalone"])
221
- .optional()
222
- .describe("inspect = Pattern A. cpi = Pattern B. standalone = verify_asset only."),
223
- assetPublicKey: z
224
- .string()
225
- .optional()
226
- .describe("Optional base64url secp256r1 pubkey to pre-derive asset PDA"),
227
- }, async ({ message, onChainPattern, assetPublicKey }) => jsonResult(await planVerifyAsset({
228
- message,
229
- assetPublicKey,
230
- onChainPattern: onChainPattern ?? "inspect",
231
- })));
232
- server.tool("list_sdk_exports", "List all major phygital-token TypeScript SDK exports and Rust client CPI types.", {}, async () => jsonResult(SDK_SURFACE));
233
- server.tool("read_sdk_source", "Read an excerpt from key SDK source files (requires PHYGITAL_TOKEN_REPO_ROOT or monorepo clone).", {
234
- file: z.enum(["verify.ts", "verifyAsset.ts", "rust_verify_asset.rs"]),
235
- maxLines: z.number().int().min(20).max(300).optional(),
236
- }, async ({ file, maxLines }) => jsonResult(await readSourceExcerpt(file, maxLines ?? 120)));
237
- server.tool("rust_cpi_verify_asset_guide", "Guide for building on verify_asset: Pattern A (inspect sysvar) or Pattern B (CPI) using phygital-token-client.", {
238
- pattern: z
239
- .enum(["inspect", "cpi"])
240
- .optional()
241
- .describe("inspect = Pattern A. cpi = Pattern B."),
242
- }, async ({ pattern = "inspect" }) => jsonResult({
243
- pattern,
244
- ...(pattern === "inspect" ? ON_CHAIN_PATTERNS.inspect : ON_CHAIN_PATTERNS.cpi),
245
- rustCrate: "phygital-token-client",
246
- path: "clients/rust/phygital-token",
247
- dependency: 'phygital-token-client = { version = "0.1", features = ["anchor"] }',
248
- cpiTypes: ["VerifyAssetCpiBuilder", "VerifyAssetInstructionArgs", "Secp256r1VerifyArgs"],
249
- clientTypeScript: pattern === "inspect"
250
- ? ["beginVerifyAsset", "completeVerifyAsset", "getVerifyAssetInstruction"]
251
- : ["beginVerifyAsset", "buildVerifyAssetArgs"],
252
- messageBinding: "Client and your program must agree on exact message bytes passed to beginVerifyAsset",
253
- referenceImplementation: pattern === "inspect"
254
- ? "programs/phygital-spend — require_matching_verify_asset"
255
- : "clients/rust/phygital-token — VerifyAssetCpiBuilder",
256
- docIds: ["building-on-phygital:overview", "building-on-phygital:rust-cpi"],
257
- testing: "programs/phygital-token/tests/verify_asset_flow.rs",
258
- note: "Off-chain tap: startAuthenticationWithChallengeResponse (client) + verifyWithChallengeResponse (server, expectedMessage + response). On-chain message binding uses beginVerifyAsset.",
259
- }));
260
- }
261
- function registerResources(server) {
262
- server.resource("verification-overview", "phygital://docs/verification/overview", {
263
- description: "Identification vs authentication — when to use which verification method",
264
- mimeType: "text/markdown",
265
- }, async () => ({
266
- contents: [
267
- {
268
- uri: "phygital://docs/verification/overview",
269
- mimeType: "text/markdown",
270
- text: await readDocById("verification:overview"),
271
- },
272
- ],
273
- }));
274
- server.resource("verify-asset-composable", "phygital://docs/verification/verify-asset-composable", {
275
- description: "Composable verify_asset flow — buildVerifyAssetArgs and transaction layout",
276
- mimeType: "text/markdown",
277
- }, async () => ({
278
- contents: [
279
- {
280
- uri: "phygital://docs/verification/verify-asset-composable",
281
- mimeType: "text/markdown",
282
- text: await readDocById("verification:verify-asset-composable"),
283
- },
284
- ],
285
- }));
286
- server.resource("building-on-phygital", "phygital://docs/building-on-phygital/overview", {
287
- description: "Guide for third-party developers building on phygital assets",
288
- mimeType: "text/markdown",
289
- }, async () => ({
290
- contents: [
291
- {
292
- uri: "phygital://docs/building-on-phygital/overview",
293
- mimeType: "text/markdown",
294
- text: await readDocById("building-on-phygital:overview"),
295
- },
296
- ],
297
- }));
298
- server.resource("gating-overview", "phygital://docs/gating/overview", {
299
- description: "Gating overview — mental model, dimensions, and tier evaluation flow",
300
- mimeType: "text/markdown",
301
- }, async () => ({
302
- contents: [
303
- {
304
- uri: "phygital://docs/gating/overview",
305
- mimeType: "text/markdown",
306
- text: await readDocById("gating:overview"),
307
- },
308
- ],
309
- }));
310
- server.resource("gating-recipes", "phygital://docs/gating/recipes", {
311
- description: "Gating copy-paste recipes and footguns",
312
- mimeType: "text/markdown",
313
- }, async () => ({
314
- contents: [
315
- {
316
- uri: "phygital://docs/gating/recipes",
317
- mimeType: "text/markdown",
318
- text: await readDocById("gating:recipes"),
319
- },
320
- ],
321
- }));
322
- server.resource("gating-predicates", "phygital://docs/gating/predicates", {
323
- description: "Gating predicates — collection, mint, traits, balance operators",
324
- mimeType: "text/markdown",
325
- }, async () => ({
326
- contents: [
327
- {
328
- uri: "phygital://docs/gating/predicates",
329
- mimeType: "text/markdown",
330
- text: await readDocById("gating:predicates"),
331
- },
332
- ],
333
- }));
334
- server.resource("gating-evaluation", "phygital://docs/gating/evaluation-and-errors", {
335
- description: "Gating evaluation results and debugging failures",
336
- mimeType: "text/markdown",
337
- }, async () => ({
338
- contents: [
339
- {
340
- uri: "phygital://docs/gating/evaluation-and-errors",
341
- mimeType: "text/markdown",
342
- text: await readDocById("gating:evaluation-and-errors"),
343
- },
344
- ],
345
- }));
346
- server.resource("glossary", "phygital://glossary", {
347
- description: "Phygital Token domain glossary (collection, design, asset, custody)",
348
- mimeType: "text/markdown",
349
- }, async () => ({
350
- contents: [
351
- {
352
- uri: "phygital://glossary",
353
- mimeType: "text/markdown",
354
- text: await readDocById("glossary"),
355
- },
356
- ],
357
- }));
358
- }
359
- function registerPrompts(server) {
360
- server.prompt("choose_verification_method", "Help pick the right phygital verification approach for a product requirement", {
361
- requirement: z
362
- .string()
363
- .describe("What you are trying to accomplish (e.g. unlock VIP door, product page, custom spend program)"),
364
- needsOnChainProof: z.boolean().optional(),
365
- onChainPattern: z.enum(["inspect", "cpi"]).optional(),
366
- isNativeApp: z.boolean().optional(),
367
- hasPriorScanUrl: z.boolean().optional(),
368
- }, async ({ requirement, needsOnChainProof, onChainPattern, isNativeApp, hasPriorScanUrl }) => ({
369
- messages: [
370
- {
371
- role: "user",
372
- content: {
373
- type: "text",
374
- text: [
375
- "Choose the right phygital-token verification method for:",
376
- requirement,
377
- needsOnChainProof != null ? `Needs on-chain proof: ${needsOnChainProof}` : "",
378
- onChainPattern ? `On-chain pattern: ${onChainPattern}` : "",
379
- isNativeApp != null ? `Native app: ${isNativeApp}` : "",
380
- hasPriorScanUrl != null ? `Has prior scan URL params: ${hasPriorScanUrl}` : "",
381
- "",
382
- "Off-chain tap: startAuthenticationWithChallengeResponse (client) + verifyWithChallengeResponse (server). No verify_asset tx.",
383
- "On-chain: Pattern A = client posts verify_asset, program inspects sysvar.",
384
- "On-chain: Pattern B = buildVerifyAssetArgs, program CPIs verify_asset.",
385
- "Use recommend_verification, explain_verification, plan_verify_asset.",
386
- ]
387
- .filter(Boolean)
388
- .join("\n"),
389
- },
390
- },
391
- ],
392
- }));
393
- server.prompt("build_composable_verify_asset_tx", "Design a composable transaction with verify_asset for a custom program", {
394
- programAction: z.string().describe("What your program does after verify_asset (spend, unlock, claim, etc.)"),
395
- onChainPattern: z
396
- .enum(["inspect", "cpi"])
397
- .describe("inspect = client posts verify_asset; cpi = program CPIs verify_asset"),
398
- messageFormat: z.string().optional().describe("Proposed message byte format to bind authorization"),
399
- }, async ({ programAction, onChainPattern, messageFormat }) => ({
400
- messages: [
401
- {
402
- role: "user",
403
- content: {
404
- type: "text",
405
- text: [
406
- `Design a Pattern ${onChainPattern === "inspect" ? "A" : "B"} verify_asset transaction for:`,
407
- programAction,
408
- messageFormat ? `Message format: ${messageFormat}` : "",
409
- "",
410
- onChainPattern === "inspect"
411
- ? "Pattern A: completeVerifyAsset + your ix; program inspects instructions sysvar."
412
- : "Pattern B: buildVerifyAssetArgs + your ix; program CPIs verify_asset.",
413
- "Use plan_verify_asset and read_doc building-on-phygital/rust-cpi.",
414
- ]
415
- .filter(Boolean)
416
- .join("\n"),
417
- },
418
- },
419
- ],
420
- }));
421
- server.prompt("design_gating_tiers", "Help design wallet gating tiers for a phygital experience", {
422
- experience: z.string().describe("What you are gating (e.g. VIP lounge, premium content)"),
423
- collections: z
424
- .string()
425
- .optional()
426
- .describe("Collection mint addresses or descriptions"),
427
- traits: z.string().optional().describe("NFT traits to consider"),
428
- }, async ({ experience, collections, traits }) => ({
429
- messages: [
430
- {
431
- role: "user",
432
- content: {
433
- type: "text",
434
- text: [
435
- "Design phygital-token gating tiers for this experience:",
436
- experience,
437
- collections ? `Collections: ${collections}` : "",
438
- traits ? `Traits: ${traits}` : "",
439
- "",
440
- "Use the Gating filter tree (count, totalBalance, and/or/not).",
441
- "Search docs with search_docs and return:",
442
- "1) recommended tier ids",
443
- "2) JSON tiers array for evaluateAssetGating in the SDK",
444
- "3) pitfalls (same asset vs same wallet)",
445
- ]
446
- .filter(Boolean)
447
- .join("\n"),
448
- },
449
- },
450
- ],
451
- }));
452
- server.prompt("debug_gating_failure", "Explain why a gating evaluation failed and how to fix it", {
453
- evaluationJson: z
454
- .string()
455
- .describe("JSON output from evaluateAssetGating or failureSummary field"),
456
- }, async ({ evaluationJson }) => ({
457
- messages: [
458
- {
459
- role: "user",
460
- content: {
461
- type: "text",
462
- text: [
463
- "Debug this phygital-token gating evaluation failure:",
464
- evaluationJson,
465
- "",
466
- "Use search_docs on evaluation-and-errors and recipes.",
467
- "Explain root cause in plain language and suggest a corrected filter tree.",
468
- ].join("\n"),
469
- },
470
- },
471
- ],
472
- }));
164
+ annotations: { title: "Plan remove_ownership", ...READ_ONLY },
165
+ }, async ({ assetPublicKey, owner, mint }) => jsonResult(await planRemoveOwnership({ assetPublicKey, owner, mint })));
166
+ server.registerTool("find_asset_pda", {
167
+ description: "Derive the on-chain asset PDA address from a secp256r1 passkey public key (offline).",
168
+ inputSchema: {
169
+ assetPublicKey: z
170
+ .string()
171
+ .describe("Base64url-encoded secp256r1 public key for the phygital asset"),
172
+ },
173
+ annotations: { title: "Find asset PDA", ...READ_ONLY },
174
+ }, async ({ assetPublicKey }) => {
175
+ const assetPda = await findAssetPda(parseSecp256r1Pubkey(assetPublicKey));
176
+ return jsonResult({ assetPublicKey, assetPda });
177
+ });
178
+ server.registerTool("list_sdk_exports", {
179
+ description: "Map of the phygital-token TypeScript SDK exports and Rust client CPI types, grouped by flow.",
180
+ inputSchema: {},
181
+ annotations: { title: "List SDK exports", ...READ_ONLY },
182
+ }, async () => jsonResult(SDK_SURFACE));
473
183
  }
474
184
  async function main() {
475
- const server = new McpServer({
476
- name: "phygital-token",
477
- version: "0.1.0",
478
- }, {
479
- instructions: SERVER_INSTRUCTIONS,
480
- });
185
+ const server = new McpServer({ name: "phygital-token", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
481
186
  registerTools(server);
482
- registerResources(server);
483
- registerPrompts(server);
484
- const transport = new StdioServerTransport();
485
- await server.connect(transport);
187
+ await server.connect(new StdioServerTransport());
486
188
  }
487
189
  main().catch((error) => {
488
190
  console.error(error);