doora-mcp 0.1.0 → 0.3.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 -9
- package/dist/api-client.js +35 -0
- package/dist/index.js +105 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,15 +48,20 @@ error when you first try to resolve a handle.
|
|
|
48
48
|
|
|
49
49
|
## Tools
|
|
50
50
|
|
|
51
|
-
| Tool | What it does | Network |
|
|
52
|
-
|
|
53
|
-
| `resolve_handle` | Look up address for a `username@label` handle | ✓ |
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
| Tool | What it does | Network | Needs `DOORA_API_KEY` |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `resolve_handle` | Look up address for a `username@label` handle | ✓ | yes |
|
|
54
|
+
| `start_handle_creation` | Mint a device-flow session so the user can create a new handle in their browser. Returns a `claim_url` to show them. | ✓ | **no** |
|
|
55
|
+
| `check_handle_status` | Poll the result of `start_handle_creation` until the user finishes. | ✓ | **no** |
|
|
56
|
+
| `encode_digipin` | lat/lng → 12-char DIGIPIN string | — | no |
|
|
57
|
+
| `decode_digipin` | DIGIPIN → lat/lng centroid | — | no |
|
|
58
|
+
| `list_demo_handles` | Curated list of test-key-resolvable handles | — | no |
|
|
59
|
+
| `validate_handle_shape` | Regex-check a handle without an API call | — | no |
|
|
60
|
+
|
|
61
|
+
The two anonymous tools (`start_handle_creation` + `check_handle_status`)
|
|
62
|
+
let an agent help a user without a doora account create one — they
|
|
63
|
+
sign in / sign up themselves in the browser; the agent only sees the
|
|
64
|
+
resulting handle string. Full flow + trust model:
|
|
60
65
|
**https://www.doora.to/docs/mcp**.
|
|
61
66
|
|
|
62
67
|
## Configuration
|
package/dist/api-client.js
CHANGED
|
@@ -66,3 +66,38 @@ export async function resolveHandle(handle) {
|
|
|
66
66
|
const trimmed = handle.trim().toLowerCase();
|
|
67
67
|
return request(`/api/v1/resolve?handle=${encodeURIComponent(trimmed)}`);
|
|
68
68
|
}
|
|
69
|
+
// ─── Claim-session (device-flow) wrappers ───────────────────────────
|
|
70
|
+
// These endpoints do NOT require DOORA_API_KEY — they're anonymous
|
|
71
|
+
// on the way in (rate-limited per IP). To skip the key check, we
|
|
72
|
+
// hit the endpoints directly with fetch instead of going through
|
|
73
|
+
// request(), which would refuse without a key.
|
|
74
|
+
const DEVICE_FLOW_BASE = () => (process.env.DOORA_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
75
|
+
const USER_AGENT = () => `doora-mcp/${process.env.npm_package_version ?? 'dev'} (+https://www.doora.to/docs/mcp)`;
|
|
76
|
+
export async function startClaimSession() {
|
|
77
|
+
const res = await fetch(`${DEVICE_FLOW_BASE()}/api/v1/mcp/claim-session`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'content-type': 'application/json', 'user-agent': USER_AGENT() },
|
|
80
|
+
body: JSON.stringify({}),
|
|
81
|
+
});
|
|
82
|
+
const body = await res.json().catch(() => null);
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
const msg = (body && typeof body === 'object' && 'error' in body)
|
|
85
|
+
? String(body.error)
|
|
86
|
+
: `HTTP ${res.status}`;
|
|
87
|
+
throw new DoraApiError(res.status, body, msg);
|
|
88
|
+
}
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
export async function checkClaimSession(sessionId) {
|
|
92
|
+
const res = await fetch(`${DEVICE_FLOW_BASE()}/api/v1/mcp/claim-session/${encodeURIComponent(sessionId)}`, {
|
|
93
|
+
headers: { 'user-agent': USER_AGENT() },
|
|
94
|
+
});
|
|
95
|
+
const body = await res.json().catch(() => null);
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const msg = (body && typeof body === 'object' && 'error' in body)
|
|
98
|
+
? String(body.error)
|
|
99
|
+
: `HTTP ${res.status}`;
|
|
100
|
+
throw new DoraApiError(res.status, body, msg);
|
|
101
|
+
}
|
|
102
|
+
return body;
|
|
103
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -45,11 +45,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
45
45
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
46
46
|
import { z } from 'zod';
|
|
47
47
|
import { encodeDigipin, decodeDigipin } from './digipin.js';
|
|
48
|
-
import { DoraApiError, resolveHandle } from './api-client.js';
|
|
48
|
+
import { DoraApiError, resolveHandle, startClaimSession, checkClaimSession, } from './api-client.js';
|
|
49
49
|
const HANDLE_PATTERN = /^([a-z0-9-]{3,24})@([a-z0-9-]{2,16})$/i;
|
|
50
50
|
const server = new McpServer({
|
|
51
51
|
name: 'doora',
|
|
52
|
-
version: '0.
|
|
52
|
+
version: '0.3.0',
|
|
53
53
|
}, {
|
|
54
54
|
capabilities: { tools: {} },
|
|
55
55
|
instructions: `doora exposes India-aware addresses behind memorable handles like \`praneeth@home\`. ` +
|
|
@@ -193,6 +193,109 @@ server.registerTool('validate_handle_shape', {
|
|
|
193
193
|
],
|
|
194
194
|
};
|
|
195
195
|
});
|
|
196
|
+
// ─── Tool 6: start_handle_creation ──────────────────────────────────
|
|
197
|
+
//
|
|
198
|
+
// Device-flow primer. The agent calls this to mint a one-time
|
|
199
|
+
// session, then asks the user to open the returned URL in their
|
|
200
|
+
// browser. The user does the visual work (pin on a map, building +
|
|
201
|
+
// floor + unit, sign-in if needed). The agent should then poll
|
|
202
|
+
// check_handle_status until it sees status='complete' and a handle
|
|
203
|
+
// string to use with resolve_handle.
|
|
204
|
+
//
|
|
205
|
+
// Anonymous — no DOORA_API_KEY required. Per-IP rate limit on the
|
|
206
|
+
// server side (3 sessions per hour) means a script can't farm
|
|
207
|
+
// these.
|
|
208
|
+
server.registerTool('start_handle_creation', {
|
|
209
|
+
title: 'Start a doora handle-creation flow for the user',
|
|
210
|
+
description: 'Begin the device flow that lets the user create a new doora handle in their ' +
|
|
211
|
+
'browser. Use this when the user asks to create / claim / make a new doora ' +
|
|
212
|
+
'address — they pick a location on a map and fill the address fields visually, ' +
|
|
213
|
+
'which neither this server nor an LLM can do directly. Returns a claim_url + a ' +
|
|
214
|
+
'session_id. Show the URL to the user and then poll check_handle_status with ' +
|
|
215
|
+
"the session_id until status='complete'. Anonymous — does NOT require " +
|
|
216
|
+
'DOORA_API_KEY; do not gate the call on key presence. Sessions expire in ' +
|
|
217
|
+
'15 minutes.',
|
|
218
|
+
inputSchema: {},
|
|
219
|
+
}, async () => {
|
|
220
|
+
try {
|
|
221
|
+
const r = await startClaimSession();
|
|
222
|
+
const expires = new Date(r.expires_at);
|
|
223
|
+
const minsLeft = Math.max(1, Math.round((expires.getTime() - Date.now()) / 60_000));
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: `Tell the user to open this URL in their browser to create a doora handle:\n\n` +
|
|
229
|
+
` ${r.claim_url}\n\n` +
|
|
230
|
+
`They'll sign in (or sign up — free), pick a location on a map, and name the handle. ` +
|
|
231
|
+
`Then call check_handle_status with this session_id to learn the new handle:\n\n` +
|
|
232
|
+
` session_id: ${r.session_id}\n\n` +
|
|
233
|
+
`Session expires in ~${minsLeft} minute(s). Poll every 3-5 seconds while waiting.`,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
return errorContent(e, 'Could not start a handle-creation session');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// ─── Tool 7: check_handle_status ────────────────────────────────────
|
|
243
|
+
//
|
|
244
|
+
// The poll endpoint. Returns one of:
|
|
245
|
+
// pending → user hasn't finished yet; keep polling.
|
|
246
|
+
// complete → got a handle; STOP polling and surface it to the user.
|
|
247
|
+
// expired → session timed out; offer to start a new one.
|
|
248
|
+
//
|
|
249
|
+
// The server's per-session rate limit (30/min) means even a tight
|
|
250
|
+
// polling loop is bounded; suggest 3-5s intervals in practice.
|
|
251
|
+
server.registerTool('check_handle_status', {
|
|
252
|
+
title: 'Poll for the result of a start_handle_creation session',
|
|
253
|
+
description: 'Poll the status of a handle-creation session returned by start_handle_creation. ' +
|
|
254
|
+
"Returns 'pending' while the user is still in the browser, 'complete' once they've " +
|
|
255
|
+
"created a handle (with the resulting username@label string), or 'expired' if the " +
|
|
256
|
+
'15-minute window has passed without completion. Anonymous — no API key required. ' +
|
|
257
|
+
"Poll every 3-5 seconds while pending; stop calling once you see 'complete' or 'expired'.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
session_id: z.string().min(16).max(64)
|
|
260
|
+
.describe('The session_id returned by start_handle_creation.'),
|
|
261
|
+
},
|
|
262
|
+
}, async ({ session_id }) => {
|
|
263
|
+
try {
|
|
264
|
+
const r = await checkClaimSession(session_id);
|
|
265
|
+
if (r.status === 'complete') {
|
|
266
|
+
return {
|
|
267
|
+
content: [{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: r.handle
|
|
270
|
+
? `✓ Done. The user created the handle \`${r.handle}\`. You can call resolve_handle('${r.handle}') to read the address (subject to test-key restrictions; see /docs/api → Demo handles).`
|
|
271
|
+
: `✓ Session complete, but no handle string was recorded. This is unusual — ask the user to verify at https://www.doora.to/me.`,
|
|
272
|
+
}],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (r.status === 'pending') {
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: `⌛ Pending — the user hasn't finished the browser flow yet. Poll again in 3-5 seconds. Expires at ${r.expires_at}.`,
|
|
280
|
+
}],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (r.status === 'expired') {
|
|
284
|
+
return {
|
|
285
|
+
content: [{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: `⌛ Expired — the 15-minute session window passed. Call start_handle_creation again to mint a fresh session if the user is still here.`,
|
|
288
|
+
}],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: 'text', text: `Session status: ${r.status ?? 'unknown'}.` }],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return errorContent(e, `Could not check session "${session_id.slice(0, 8)}…"`);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
196
299
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
197
300
|
function formatResolveResult(r) {
|
|
198
301
|
// The address payload is most readable when pretty-printed as JSON
|