pion-mcp 0.1.1 → 0.2.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
@@ -3,7 +3,7 @@
3
3
  **Model Context Protocol server for Pi Network** — connect AI agents
4
4
  (Claude, Cursor, and any MCP-compatible client) to Pi Network chain data.
5
5
 
6
- > ⚠️ v0.1 — testnet, read-only.
6
+ > ⚠️ v0.2 — testnet, read-only. Nothing here can move value.
7
7
 
8
8
  ## Why "Pion"?
9
9
  The pion is the π meson — the particle physicists named after pi.
@@ -12,9 +12,11 @@ for MCPs (millicharged particles). We couldn't resist.
12
12
 
13
13
  ## Tools
14
14
 
15
- All three are zero-permission reads against Pi's public Horizon API
16
- (Tier A in [`docs/tool-mapping.md`](docs/tool-mapping.md)). **No API keys, no
17
- wallet secrets, no user consent** — and nothing here can move value.
15
+ Nothing here can move value. No server API key or wallet secret is read
16
+ anywhere. (Tiers refer to [`docs/tool-mapping.md`](docs/tool-mapping.md).)
17
+
18
+ **Tier A — chain reads.** Zero-permission queries against Pi's public Horizon
19
+ API. No credentials at all.
18
20
 
19
21
  | Tool | What it does |
20
22
  |---|---|
@@ -25,6 +27,23 @@ wallet secrets, no user consent** — and nothing here can move value.
25
27
  Amounts are decimal strings. Pi is reported as the asset `PI`, custom tokens as
26
28
  `CODE:ISSUER`, and liquidity-pool shares as `pool:ID`.
27
29
 
30
+ **Tier B — identity.**
31
+
32
+ | Tool | What it does |
33
+ |---|---|
34
+ | `verify_user` | Validate a Pi user access token, returning the uid and username |
35
+
36
+ `verify_user` is the only tool that touches a credential, and it never holds
37
+ one: the caller passes a token per call, it goes to `GET /v2/me` and nowhere
38
+ else, and it is not stored, logged, or echoed back. A rejected token returns
39
+ `valid: false` with a reason rather than erroring, so an agent can branch on
40
+ the outcome.
41
+
42
+ Two caveats worth knowing. The `uid` is **app-specific** — the same person has
43
+ a different uid under a different Pi app, which is deliberate anti-correlation
44
+ design, so don't use it as a global identifier. And a token is the *only* proof
45
+ of identity: a client-supplied uid or username means nothing on its own.
46
+
28
47
  ## Usage
29
48
 
30
49
  MCP clients can run it straight from npm — no install step:
@@ -59,8 +78,10 @@ claude mcp add pion -- node /absolute/path/to/pion-mcp/dist/index.js
59
78
  | Variable | Default | Purpose |
60
79
  |---|---|---|
61
80
  | `PION_HORIZON_URL` | `https://api.testnet.minepi.com` | Horizon base URL |
81
+ | `PION_PLATFORM_URL` | `https://api.minepi.com` | Platform API base URL |
62
82
 
63
- There are no secrets to configure. The mainnet Horizon URL is still an open
83
+ There are no secrets to configure `verify_user` takes its token as a call
84
+ argument, not from the environment. The mainnet Horizon URL is still an open
64
85
  question — see the TODO in [`docs/pi-sdk-notes.md`](docs/pi-sdk-notes.md).
65
86
 
66
87
  ## Development
@@ -72,13 +93,22 @@ npm run smoke # end-to-end: drives the built server against live testnet
72
93
  ```
73
94
 
74
95
  `npm run smoke` spawns the server over stdio as a real MCP client, discovers a
75
- funded account from the current ledger, and exercises all three tools plus the
96
+ funded account from the current ledger, and exercises the chain tools plus the
76
97
  not-found and invalid-input paths. It needs network access.
77
98
 
99
+ It covers `verify_user` only on the **rejection** path — confirming a genuine
100
+ token would need a real user credential, which the test deliberately does not
101
+ handle. The success path is unverified; see below.
102
+
78
103
  ## Roadmap
79
104
 
80
- v0.2 adds Tier B/C behind env config (`PI_SERVER_API_KEY`, `PI_WALLET_SECRET`):
81
- user verification and App-to-User payments, testnet-default with explicit
82
- opt-in for anything that moves value. See [`docs/tool-mapping.md`](docs/tool-mapping.md).
105
+ Tier C is next: App-to-User payments behind env config (`PI_SERVER_API_KEY`,
106
+ `PI_WALLET_SECRET`), testnet-default with explicit opt-in for anything that
107
+ moves value. See [`docs/tool-mapping.md`](docs/tool-mapping.md).
108
+
109
+ Known gap: `verify_user`'s success-path response shape is built from the Pi
110
+ platform docs, not observed traffic. The `uid` field is reliable; `username`
111
+ and `credentials` depend on granted scopes and are treated as optional. Worth
112
+ confirming against a real token before depending on them.
83
113
 
84
114
  *Unofficial community project — not affiliated with Pi Network.*
package/dist/index.js CHANGED
@@ -2,17 +2,24 @@
2
2
  /**
3
3
  * Pion — MCP server for Pi Network.
4
4
  *
5
- * v0.1 scope: Tier A only (see docs/tool-mapping.md) — zero-permission,
6
- * read-only chain queries against Pi's public Horizon API. No API keys, no
7
- * wallet secrets, no user consent required, and nothing here can move value.
5
+ * Scope (see docs/tool-mapping.md):
6
+ * Tier A — zero-permission, read-only chain queries against Pi's public
7
+ * Horizon API. No credentials of any kind.
8
+ * Tier B — verify_user, which validates a *user* access token supplied by
9
+ * the caller against the Platform API.
10
+ *
11
+ * No server API key or wallet secret is read anywhere in this server, and no
12
+ * tool can move value.
8
13
  */
9
14
  import { createRequire } from "node:module";
10
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
17
  import { HORIZON_URL } from "./horizon.js";
18
+ import { PLATFORM_URL } from "./platform.js";
13
19
  import { registerGetAccountPayments } from "./tools/get-account-payments.js";
14
20
  import { registerGetWalletBalance } from "./tools/get-wallet-balance.js";
15
21
  import { registerQueryTransaction } from "./tools/query-transaction.js";
22
+ import { registerVerifyUser } from "./tools/verify-user.js";
16
23
  // Single source of truth for the version. `../package.json` resolves to the
17
24
  // package root from both dist/index.js and src/index.ts, so this is correct
18
25
  // whether running the build or the sources directly. Resolved at runtime
@@ -30,23 +37,28 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
30
37
  "Runs an MCP server over stdio. Point an MCP client at it rather than",
31
38
  "invoking it directly.",
32
39
  "",
33
- "Tools: get_wallet_balance, get_account_payments, query_transaction",
40
+ "Tools: get_wallet_balance, get_account_payments, query_transaction, verify_user",
34
41
  "",
35
42
  "Environment:",
36
- " PION_HORIZON_URL Horizon base URL (default: https://api.testnet.minepi.com)",
43
+ " PION_HORIZON_URL Horizon base URL (default: https://api.testnet.minepi.com)",
44
+ " PION_PLATFORM_URL Platform API base URL (default: https://api.minepi.com)",
37
45
  "",
38
46
  ].join("\n"));
39
47
  process.exit(0);
40
48
  }
41
49
  const server = new McpServer({ name: "pion-mcp", version: VERSION }, {
42
- instructions: `Pion exposes read-only Pi Network chain data from Horizon at ${HORIZON_URL} ` +
43
- `(${NETWORK}). All three tools are public ledger reads: they cannot send payments, ` +
44
- "sign anything, or access a user's wallet. Amounts are decimal strings; Pi itself " +
45
- 'is reported as the asset "PI" and custom tokens as "CODE:ISSUER".',
50
+ instructions: `Pion exposes read-only Pi Network data. get_wallet_balance, get_account_payments, ` +
51
+ `and query_transaction are public ledger reads from Horizon at ${HORIZON_URL} ` +
52
+ `(${NETWORK}), needing no credentials. Amounts are decimal strings; Pi itself is ` +
53
+ 'reported as the asset "PI", custom tokens as "CODE:ISSUER", and liquidity-pool ' +
54
+ 'shares as "pool:ID". verify_user is different: it checks a user access token ' +
55
+ `against the Pi Platform API at ${PLATFORM_URL} and requires the caller to supply ` +
56
+ "that token. No tool here can send payments, sign anything, or spend from a wallet.",
46
57
  });
47
58
  registerGetWalletBalance(server, NETWORK);
48
59
  registerGetAccountPayments(server, NETWORK);
49
60
  registerQueryTransaction(server, NETWORK);
61
+ registerVerifyUser(server);
50
62
  async function main() {
51
63
  // stdout is the JSON-RPC channel — every log line must go to stderr.
52
64
  await server.connect(new StdioServerTransport());
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExE,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,sEAAsE;AACtE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAE5E,CAAC;AAEF,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,WAAW,GAAG,CAAC;AAE3F,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,YAAY,OAAO,0CAA0C;QAC7D,EAAE;QACF,sEAAsE;QACtE,uBAAuB;QACvB,EAAE;QACF,oEAAoE;QACpE,EAAE;QACF,cAAc;QACd,gFAAgF;QAChF,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EACtC;IACE,YAAY,EACV,gEAAgE,WAAW,GAAG;QAC9E,IAAI,OAAO,yEAAyE;QACpF,mFAAmF;QACnF,mEAAmE;CACtE,CACF,CAAC;AAEF,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,0BAA0B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC5C,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,qEAAqE;IACrE,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,YAAY,OAAO,8BAA8B,WAAW,KAAK,OAAO,GAAG,CAAC,CAAC;AAC7F,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,sEAAsE;AACtE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAE5E,CAAC;AAEF,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,WAAW,GAAG,CAAC;AAE3F,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,YAAY,OAAO,0CAA0C;QAC7D,EAAE;QACF,sEAAsE;QACtE,uBAAuB;QACvB,EAAE;QACF,iFAAiF;QACjF,EAAE;QACF,cAAc;QACd,iFAAiF;QACjF,8EAA8E;QAC9E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EACtC;IACE,YAAY,EACV,oFAAoF;QACpF,iEAAiE,WAAW,GAAG;QAC/E,IAAI,OAAO,uEAAuE;QAClF,iFAAiF;QACjF,+EAA+E;QAC/E,kCAAkC,YAAY,qCAAqC;QACnF,oFAAoF;CACvF,CACF,CAAC;AAEF,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,0BAA0B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC5C,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,kBAAkB,CAAC,MAAM,CAAC,CAAC;AAE3B,KAAK,UAAU,IAAI;IACjB,qEAAqE;IACrE,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,YAAY,OAAO,8BAA8B,WAAW,KAAK,OAAO,GAAG,CAAC,CAAC;AAC7F,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Minimal client for the Pi Platform API (Layer 2 in docs/pi-sdk-notes.md).
3
+ *
4
+ * Unlike Horizon, these endpoints are authenticated. This module only ever
5
+ * handles a *user* access token, which the caller supplies per request — it
6
+ * never reads a server API key or wallet secret, and it never persists,
7
+ * logs, or echoes back a token.
8
+ */
9
+ /** Platform API base URL. Override with PION_PLATFORM_URL. */
10
+ export declare const PLATFORM_URL: string;
11
+ /** A Platform API request that failed for reasons other than a bad token. */
12
+ export declare class PlatformError extends Error {
13
+ readonly status?: number | undefined;
14
+ constructor(message: string, status?: number | undefined);
15
+ }
16
+ /**
17
+ * The token was rejected (401/403). This is a normal, expected answer to
18
+ * "is this token valid?" — not a malfunction — so callers report it as a
19
+ * result rather than an error.
20
+ */
21
+ export declare class PlatformAuthError extends Error {
22
+ readonly status: number;
23
+ constructor(message: string, status: number);
24
+ }
25
+ export declare function platformGet<T>(path: string, accessToken: string): Promise<T>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Minimal client for the Pi Platform API (Layer 2 in docs/pi-sdk-notes.md).
3
+ *
4
+ * Unlike Horizon, these endpoints are authenticated. This module only ever
5
+ * handles a *user* access token, which the caller supplies per request — it
6
+ * never reads a server API key or wallet secret, and it never persists,
7
+ * logs, or echoes back a token.
8
+ */
9
+ const DEFAULT_PLATFORM_URL = "https://api.minepi.com";
10
+ const REQUEST_TIMEOUT_MS = 15_000;
11
+ /** Platform API base URL. Override with PION_PLATFORM_URL. */
12
+ export const PLATFORM_URL = (process.env.PION_PLATFORM_URL ?? DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
13
+ /** A Platform API request that failed for reasons other than a bad token. */
14
+ export class PlatformError extends Error {
15
+ status;
16
+ constructor(message, status) {
17
+ super(message);
18
+ this.status = status;
19
+ this.name = "PlatformError";
20
+ }
21
+ }
22
+ /**
23
+ * The token was rejected (401/403). This is a normal, expected answer to
24
+ * "is this token valid?" — not a malfunction — so callers report it as a
25
+ * result rather than an error.
26
+ */
27
+ export class PlatformAuthError extends Error {
28
+ status;
29
+ constructor(message, status) {
30
+ super(message);
31
+ this.status = status;
32
+ this.name = "PlatformAuthError";
33
+ }
34
+ }
35
+ export async function platformGet(path, accessToken) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
38
+ let response;
39
+ try {
40
+ response = await fetch(`${PLATFORM_URL}${path}`, {
41
+ headers: {
42
+ authorization: `Bearer ${accessToken}`,
43
+ accept: "application/json",
44
+ },
45
+ signal: controller.signal,
46
+ });
47
+ }
48
+ catch (err) {
49
+ if (controller.signal.aborted) {
50
+ throw new PlatformError(`Platform API request timed out after ${REQUEST_TIMEOUT_MS}ms`);
51
+ }
52
+ // `err` can echo the request; report only its message, never the headers.
53
+ throw new PlatformError(`Could not reach the Pi Platform API at ${PLATFORM_URL}: ${err.message}`);
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ if (response.status === 401 || response.status === 403) {
59
+ throw new PlatformAuthError(response.status === 401
60
+ ? "The Pi Platform API rejected this access token. It is invalid, expired, or was issued for a different app."
61
+ : "This access token is valid but lacks the scope required for this call.", response.status);
62
+ }
63
+ if (!response.ok) {
64
+ // 401 responses come back with an empty body, so never assume JSON here.
65
+ const body = await response.text().catch(() => "");
66
+ const detail = body.trim().slice(0, 300);
67
+ throw new PlatformError(detail.length > 0
68
+ ? `Pi Platform API returned ${response.status}: ${detail}`
69
+ : `Pi Platform API returned ${response.status} ${response.statusText} for ${path}`, response.status);
70
+ }
71
+ return (await response.json());
72
+ }
73
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,oBAAoB,GAAG,wBAAwB,CAAC;AACtD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,8DAA8D;AAC9D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,oBAAoB,CAAC,CAAC,OAAO,CACzF,MAAM,EACN,EAAE,CACH,CAAC;AAEF,6EAA6E;AAC7E,MAAM,OAAO,aAAc,SAAQ,KAAK;IAG3B,MAAM;IAFjB,YACE,OAAe,EACN,MAAe;QAExB,KAAK,CAAC,OAAO,CAAC,CAAC;sBAFN,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAG/B,MAAM;IAFjB,YACE,OAAe,EACN,MAAc;QAEvB,KAAK,CAAC,OAAO,CAAC,CAAC;sBAFN,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAI,IAAY,EAAE,WAAmB;IACpE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,kBAAkB,CAAC,CAAC;IAEvE,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,YAAY,GAAG,IAAI,EAAE,EAAE;YAC/C,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,WAAW,EAAE;gBACtC,MAAM,EAAE,kBAAkB;aAC3B;YACD,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,IAAI,aAAa,CAAC,wCAAwC,kBAAkB,IAAI,CAAC,CAAC;QAC1F,CAAC;QACD,0EAA0E;QAC1E,MAAM,IAAI,aAAa,CACrB,0CAA0C,YAAY,KAAM,GAAa,CAAC,OAAO,EAAE,CACpF,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvD,MAAM,IAAI,iBAAiB,CACzB,QAAQ,CAAC,MAAM,KAAK,GAAG;YACrB,CAAC,CAAC,4GAA4G;YAC9G,CAAC,CAAC,wEAAwE,EAC5E,QAAQ,CAAC,MAAM,CAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,yEAAyE;QACzE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACzC,MAAM,IAAI,aAAa,CACrB,MAAM,CAAC,MAAM,GAAG,CAAC;YACf,CAAC,CAAC,4BAA4B,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE;YAC1D,CAAC,CAAC,4BAA4B,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,QAAQ,IAAI,EAAE,EACpF,QAAQ,CAAC,MAAM,CAChB,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;AACtC,CAAC"}
@@ -12,5 +12,8 @@ export declare const pagingOrder: z.ZodDefault<z.ZodEnum<{
12
12
  }>>;
13
13
  /** A successful tool result: JSON text for humans, structured content for agents. */
14
14
  export declare function ok<T extends Record<string, unknown>>(data: T): CallToolResult;
15
- /** A failed tool result. `isError` keeps the failure inside the conversation. */
15
+ /**
16
+ * A failed tool result. `isError` keeps the failure inside the conversation
17
+ * so the agent can react, rather than faulting the transport.
18
+ */
16
19
  export declare function fail(error: unknown): CallToolResult;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { HORIZON_URL, HorizonError } from "../horizon.js";
2
+ import { HorizonError } from "../horizon.js";
3
+ import { PlatformError } from "../platform.js";
3
4
  /** Stellar/Pi public key: 56 base32 characters beginning with G. */
4
5
  export const walletAddress = z
5
6
  .string()
@@ -32,11 +33,14 @@ export function ok(data) {
32
33
  structuredContent: data,
33
34
  };
34
35
  }
35
- /** A failed tool result. `isError` keeps the failure inside the conversation. */
36
+ /**
37
+ * A failed tool result. `isError` keeps the failure inside the conversation
38
+ * so the agent can react, rather than faulting the transport.
39
+ */
36
40
  export function fail(error) {
37
- const message = error instanceof HorizonError
41
+ const message = error instanceof HorizonError || error instanceof PlatformError
38
42
  ? error.message
39
- : `Unexpected error querying ${HORIZON_URL}: ${error instanceof Error ? error.message : String(error)}`;
43
+ : `Unexpected error: ${error instanceof Error ? error.message : String(error)}`;
40
44
  return { content: [{ type: "text", text: message }], isError: true };
41
45
  }
42
46
  //# sourceMappingURL=common.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/tools/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1D,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,KAAK,CACJ,iBAAiB,EACjB,4EAA4E,CAC7E;KACA,QAAQ,CAAC,sEAAsE,CAAC,CAAC;AAEpF,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,KAAK,CAAC,mBAAmB,EAAE,6CAA6C,CAAC;KACzE,QAAQ,CAAC,sCAAsC,CAAC,CAAC;AAEpD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,EAAE;KACR,GAAG,EAAE;KACL,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,OAAO,CAAC,EAAE,CAAC;KACX,QAAQ,CAAC,qDAAqD,CAAC,CAAC;AAEnE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC;KAC1B,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,QAAQ,CAAC,8EAA8E,CAAC,CAAC;AAE5F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;KACrB,OAAO,CAAC,MAAM,CAAC;KACf,QAAQ,CAAC,gFAAgF,CAAC,CAAC;AAE9F,qFAAqF;AACrF,MAAM,UAAU,EAAE,CAAoC,IAAO;IAC3D,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QAChE,iBAAiB,EAAE,IAAI;KACxB,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,IAAI,CAAC,KAAc;IACjC,MAAM,OAAO,GACX,KAAK,YAAY,YAAY;QAC3B,CAAC,CAAC,KAAK,CAAC,OAAO;QACf,CAAC,CAAC,6BAA6B,WAAW,KACtC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CAAC;IACT,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/tools/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,KAAK,CACJ,iBAAiB,EACjB,4EAA4E,CAC7E;KACA,QAAQ,CAAC,sEAAsE,CAAC,CAAC;AAEpF,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,KAAK,CAAC,mBAAmB,EAAE,6CAA6C,CAAC;KACzE,QAAQ,CAAC,sCAAsC,CAAC,CAAC;AAEpD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,EAAE;KACR,GAAG,EAAE;KACL,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,OAAO,CAAC,EAAE,CAAC;KACX,QAAQ,CAAC,qDAAqD,CAAC,CAAC;AAEnE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC;KAC1B,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,QAAQ,CAAC,8EAA8E,CAAC,CAAC;AAE5F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;KACrB,OAAO,CAAC,MAAM,CAAC;KACf,QAAQ,CAAC,gFAAgF,CAAC,CAAC;AAE9F,qFAAqF;AACrF,MAAM,UAAU,EAAE,CAAoC,IAAO;IAC3D,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QAChE,iBAAiB,EAAE,IAAI;KACxB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,KAAc;IACjC,MAAM,OAAO,GACX,KAAK,YAAY,YAAY,IAAI,KAAK,YAAY,aAAa;QAC7D,CAAC,CAAC,KAAK,CAAC,OAAO;QACf,CAAC,CAAC,qBAAqB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACpF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerVerifyUser(server: McpServer): void;
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { PlatformAuthError, PLATFORM_URL, platformGet } from "../platform.js";
3
+ import { fail, ok } from "./common.js";
4
+ const outputSchema = {
5
+ valid: z.boolean(),
6
+ uid: z.string().optional(),
7
+ username: z.string().optional(),
8
+ scopes: z.array(z.string()).optional(),
9
+ valid_until: z.string().optional(),
10
+ reason: z.string().optional(),
11
+ };
12
+ export function registerVerifyUser(server) {
13
+ server.registerTool("verify_user", {
14
+ title: "Verify a Pi user access token",
15
+ description: "Check whether a Pi user access token is genuine and, if so, who it belongs to. " +
16
+ "Call this to authenticate someone who claims a Pi identity — never trust a " +
17
+ "client-supplied uid or username on its own; this is the only thing that proves it. " +
18
+ "Returns `valid: false` with a reason for a rejected token rather than failing. " +
19
+ `Sends the token to the Pi Platform API (${PLATFORM_URL}/v2/me) and nothing else; ` +
20
+ "it is not stored or logged. Note the uid is app-specific — the same person has a " +
21
+ "different uid under a different Pi app.",
22
+ inputSchema: {
23
+ access_token: z
24
+ .string()
25
+ .min(1)
26
+ .describe("The user's Pi access token, obtained from Pi Browser authentication or Pi " +
27
+ "Sign-in OAuth. This is a credential — pass the token itself, not a uid."),
28
+ },
29
+ outputSchema,
30
+ annotations: { readOnlyHint: true, openWorldHint: true },
31
+ }, async ({ access_token }) => {
32
+ try {
33
+ const me = await platformGet("/v2/me", access_token);
34
+ return ok({
35
+ valid: true,
36
+ uid: me.uid,
37
+ ...(me.username !== undefined ? { username: me.username } : {}),
38
+ ...(me.credentials?.scopes !== undefined ? { scopes: me.credentials.scopes } : {}),
39
+ ...(me.credentials?.valid_until?.iso8601 !== undefined
40
+ ? { valid_until: me.credentials.valid_until.iso8601 }
41
+ : {}),
42
+ });
43
+ }
44
+ catch (error) {
45
+ // A rejected token is a real answer to "is this valid?", not a fault.
46
+ if (error instanceof PlatformAuthError) {
47
+ return ok({ valid: false, reason: error.message });
48
+ }
49
+ return fail(error);
50
+ }
51
+ });
52
+ }
53
+ //# sourceMappingURL=verify-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-user.js","sourceRoot":"","sources":["../../src/tools/verify-user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAYvC,MAAM,YAAY,GAAG;IACnB,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE;IAClB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACtC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAAC,MAAiB;IAClD,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,+BAA+B;QACtC,WAAW,EACT,iFAAiF;YACjF,6EAA6E;YAC7E,qFAAqF;YACrF,iFAAiF;YACjF,2CAA2C,YAAY,4BAA4B;YACnF,mFAAmF;YACnF,yCAAyC;QAC3C,WAAW,EAAE;YACX,YAAY,EAAE,CAAC;iBACZ,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CACP,4EAA4E;gBAC1E,yEAAyE,CAC5E;SACJ;QACD,YAAY;QACZ,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE;KACzD,EACD,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,WAAW,CAAa,QAAQ,EAAE,YAAY,CAAC,CAAC;YACjE,OAAO,EAAE,CAAC;gBACR,KAAK,EAAE,IAAI;gBACX,GAAG,EAAE,EAAE,CAAC,GAAG;gBACX,GAAG,CAAC,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/D,GAAG,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClF,GAAG,CAAC,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,KAAK,SAAS;oBACpD,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,OAAO,EAAE;oBACrD,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,sEAAsE;YACtE,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;gBACvC,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACrD,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pion-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Pion — Model Context Protocol (MCP) server for Pi Network. Read-only chain queries against Pi testnet Horizon.",
5
5
  "keywords": [
6
6
  "mcp",