@chaindaddy/mcp 0.6.3 → 0.6.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaindaddy/mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "MCP server for Chain Daddy: search tickers, launch a token or verify ownership of one, and run its creator store",
|
|
5
5
|
"mcpName": "io.chaindaddy/mcp",
|
|
6
6
|
"homepage": "https://docs.chaindaddy.io/developer/store-and-rewards#tools-for-agents",
|
|
@@ -51,7 +51,7 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
|
|
|
51
51
|
document.querySelector('cd-signin-button').addEventListener('cd-signin', async (e) => {
|
|
52
52
|
const { id_token } = await ChainDaddy.exchangeCode(e.detail);
|
|
53
53
|
await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
54
|
-
body: JSON.stringify({ id_token
|
|
54
|
+
body: JSON.stringify({ id_token }) });
|
|
55
55
|
location.reload();
|
|
56
56
|
});
|
|
57
57
|
</script>
|
|
@@ -66,7 +66,7 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
|
|
|
66
66
|
if (!result) return; // popup: handed back and closed
|
|
67
67
|
const { id_token } = await ChainDaddy.exchangeCode(result); // popup blocked: finish here
|
|
68
68
|
await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
body: JSON.stringify({ id_token
|
|
69
|
+
body: JSON.stringify({ id_token }) });
|
|
70
70
|
location.replace('/');
|
|
71
71
|
});
|
|
72
72
|
</script>
|
|
@@ -77,18 +77,36 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
|
|
|
77
77
|
```ts
|
|
78
78
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
79
79
|
const JWKS = createRemoteJWKSet(new URL('https://api.chaindaddy.io/api/v2/oauth/jwks'));
|
|
80
|
+
// ID tokens already accepted, until they expire. A real deployment keeps these in its session store or Redis.
|
|
81
|
+
const seen = new Map<string, number>();
|
|
80
82
|
|
|
81
|
-
export async function verifyChainDaddy(idToken: string
|
|
83
|
+
export async function verifyChainDaddy(idToken: string) {
|
|
82
84
|
const { payload } = await jwtVerify(idToken, JWKS, {
|
|
83
85
|
issuer: 'https://api.chaindaddy.io', audience: 'https://YOUR_SITE/client.json', algorithms: ['ES256'],
|
|
84
86
|
});
|
|
85
87
|
if (payload.typ !== 'signin+v1') throw new Error('not a Chain Daddy sign-in');
|
|
86
|
-
|
|
88
|
+
const now = Date.now() / 1000;
|
|
89
|
+
seen.forEach((exp, jti) => { if (exp < now) seen.delete(jti); });
|
|
90
|
+
if (!payload.jti || seen.has(payload.jti)) throw new Error('replayed sign-in');
|
|
91
|
+
seen.set(payload.jti, payload.exp ?? now + 300);
|
|
87
92
|
return payload; // sub: the person's id at this site; wallets[0]: the wallet they signed in with
|
|
88
93
|
}
|
|
89
94
|
```
|
|
90
95
|
|
|
91
|
-
To require a coin, add `
|
|
96
|
+
To require a coin, add `"chaindaddy_claims": {"id_token":{"holdings":{"tokens":[{"chain":"base","address":"0xTOKEN","min":"100"}]}}}` to `client.json`. Chain Daddy then answers that request and no other, and refuses a sign-in that asks for anything else; the button needs no `claims`. The server still never reads `payload.holdings[0].held`: it matches each answer against its own constants first.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// What this site gates on, as the ID token writes it: token lowercase for an EVM address, min "0" if none was asked.
|
|
100
|
+
const GATE = [{ chain: 'base', token: '0xtoken', min: '100' }];
|
|
101
|
+
|
|
102
|
+
export function holdsCoin(payload: Record<string, unknown>) {
|
|
103
|
+
const holdings = Array.isArray(payload.holdings) ? payload.holdings : [];
|
|
104
|
+
return GATE.some((want) => holdings.some((h) =>
|
|
105
|
+
h?.chain === want.chain && h?.token === want.token && h?.min === want.min && h?.held === true));
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`held` is `null` when the balance could not be read: ask again, never treat it as yes. `{"id_token":{"crowns":{"all":true}}}` lists the crowns they hold.
|
|
92
110
|
|
|
93
111
|
## 1b. Or register a client (the human does this)
|
|
94
112
|
|
|
@@ -157,9 +175,11 @@ that throws unless `typ === 'signin+v1'`.
|
|
|
157
175
|
| `profile` | `identity`: the handle they chose to publish |
|
|
158
176
|
|
|
159
177
|
With the `claims` request (section 1): `holdings` (`held` true, false, or `null` when unreadable) and `crowns`.
|
|
178
|
+
Match a `holdings` entry against your own coin and minimum before reading `held` (section 1).
|
|
160
179
|
|
|
161
180
|
Check on every ID token: signature (ES256, keys above), `iss` = `https://api.chaindaddy.io`, `aud` = your
|
|
162
|
-
client id, `typ` = `signin+v1`, `nonce` = the one
|
|
181
|
+
client id, `typ` = `signin+v1`, `exp` (5 minutes), and `nonce` = the one your server sent. With the button the
|
|
182
|
+
browser makes the nonce, so comparing it proves nothing: refuse a `jti` you have already accepted (section 1).
|
|
163
183
|
|
|
164
184
|
## After sign-in
|
|
165
185
|
|