@chaindaddy/mcp 0.6.6 → 0.6.7
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.7",
|
|
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",
|
|
@@ -47,11 +47,17 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
|
|
|
47
47
|
<cd-signin-button client-id="https://YOUR_SITE/client.json"
|
|
48
48
|
redirect-uri="https://YOUR_SITE/callback.html"
|
|
49
49
|
scope="openid wallets"></cd-signin-button>
|
|
50
|
+
<p id="signin-error" role="alert"></p>
|
|
50
51
|
<script>
|
|
51
52
|
document.querySelector('cd-signin-button').addEventListener('cd-signin', async (e) => {
|
|
52
53
|
const { id_token } = await ChainDaddy.exchangeCode(e.detail);
|
|
53
|
-
await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
const res = await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
54
55
|
body: JSON.stringify({ id_token }) });
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const { message } = await res.json().catch(() => ({}));
|
|
58
|
+
document.querySelector('#signin-error').textContent = message || 'Sign-in did not work. Try again.';
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
55
61
|
location.reload();
|
|
56
62
|
});
|
|
57
63
|
</script>
|
|
@@ -61,20 +67,28 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
|
|
|
61
67
|
|
|
62
68
|
```html
|
|
63
69
|
<script src="https://js.chaindaddy.io/v1/cd.js"></script>
|
|
70
|
+
<p id="signin-error" role="alert"></p>
|
|
64
71
|
<script>
|
|
65
72
|
ChainDaddy.handleCallback().then(async (result) => {
|
|
66
73
|
if (!result) return; // popup: handed back and closed
|
|
67
74
|
const { id_token } = await ChainDaddy.exchangeCode(result); // popup blocked: finish here
|
|
68
|
-
await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
const res = await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
69
76
|
body: JSON.stringify({ id_token }) });
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const { message } = await res.json().catch(() => ({}));
|
|
79
|
+
document.querySelector('#signin-error').textContent = message || 'Sign-in did not work. Try again.';
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
70
82
|
location.replace('/');
|
|
71
83
|
});
|
|
72
84
|
</script>
|
|
73
85
|
```
|
|
74
86
|
|
|
75
|
-
4. On the server,
|
|
87
|
+
4. On the server, an Express app (`npm install express jose`): verify the ID token, then start your own session (never trust the token in the browser). `POST /session` answers a refusal with a status and a `message`, which both pages show:
|
|
76
88
|
|
|
77
89
|
```ts
|
|
90
|
+
import express from 'express';
|
|
91
|
+
import { randomUUID } from 'node:crypto';
|
|
78
92
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
79
93
|
const JWKS = createRemoteJWKSet(new URL('https://api.chaindaddy.io/api/v2/oauth/jwks'));
|
|
80
94
|
// ID tokens already accepted, until they expire. A real deployment keeps these in its session store or Redis.
|
|
@@ -91,6 +105,36 @@ export async function verifyChainDaddy(idToken: string) {
|
|
|
91
105
|
seen.set(payload.jti, payload.exp ?? now + 300);
|
|
92
106
|
return payload; // sub: the person's id at this site; wallets[0]: the wallet they signed in with
|
|
93
107
|
}
|
|
108
|
+
|
|
109
|
+
// Your own sessions, by the id in the cookie. A real deployment keeps these in its session store or Redis.
|
|
110
|
+
const sessions = new Map<string, { sub: string; wallet?: string }>();
|
|
111
|
+
const sessionOf = (req: express.Request) =>
|
|
112
|
+
sessions.get(/(?:^|; )session=([^;]+)/.exec(req.headers.cookie ?? '')?.[1] ?? '');
|
|
113
|
+
|
|
114
|
+
const app = express();
|
|
115
|
+
app.use(express.json());
|
|
116
|
+
|
|
117
|
+
app.post('/session', async (req, res) => {
|
|
118
|
+
let payload;
|
|
119
|
+
try {
|
|
120
|
+
payload = await verifyChainDaddy(req.body.id_token);
|
|
121
|
+
} catch {
|
|
122
|
+
return res.status(401).json({ error: 'sign_in_failed', message: 'That sign-in could not be checked. Sign in again.' });
|
|
123
|
+
}
|
|
124
|
+
const id = randomUUID();
|
|
125
|
+
sessions.set(id, { sub: payload.sub as string, wallet: (payload.wallets as string[] | undefined)?.[0] });
|
|
126
|
+
res.cookie('session', id, { httpOnly: true, secure: true, sameSite: 'lax' });
|
|
127
|
+
res.json({ ok: true });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Only a signed-in visitor gets this.
|
|
131
|
+
app.get('/members', (req, res) => {
|
|
132
|
+
const user = sessionOf(req);
|
|
133
|
+
if (!user) return res.status(401).json({ error: 'signed_out', message: 'Sign in first.' });
|
|
134
|
+
res.json({ sub: user.sub, wallet: user.wallet });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
app.listen(3000);
|
|
94
138
|
```
|
|
95
139
|
|
|
96
140
|
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.
|
|
@@ -106,6 +150,20 @@ export function holdsCoin(payload: Record<string, unknown>) {
|
|
|
106
150
|
}
|
|
107
151
|
```
|
|
108
152
|
|
|
153
|
+
In `POST /session`, refuse right after `verifyChainDaddy`, so no session is made and the page shows the `message`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
if (!holdsCoin(payload)) {
|
|
157
|
+
// held is null when the balance could not be read: not a no, so ask them to sign in again.
|
|
158
|
+
const unread = Array.isArray(payload.holdings) && payload.holdings.some((h) => h?.held === null);
|
|
159
|
+
return unread
|
|
160
|
+
? res.status(503).json({ error: 'balance_unreadable', message: 'We could not read your balance just now. Sign in again in a moment.' })
|
|
161
|
+
: res.status(403).json({ error: 'not_a_holder', message: 'This page is for holders of the coin.' });
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The token address is `tokenAddress` in `GET https://api.chaindaddy.io/api/v2/public/coins/{chain}/{symbol}` (keyless; e.g. `/api/v2/public/coins/base/FLUXGP`). `holdsCoin` reads only the payload, so test it without a wallet: call it with `{ holdings: [{ chain, token, min, held }] }`. `held: true` must pass; `held: false`, another token's `held: true` and `held: null` must not.
|
|
166
|
+
|
|
109
167
|
`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.
|
|
110
168
|
|
|
111
169
|
## 1b. Or register a client (the human does this)
|