doora-mcp 0.1.0 → 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.
@@ -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.1.0',
52
+ version: '0.2.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doora-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for doora — let Claude / Cursor / Continue look up addresses by doora handle.",
5
5
  "keywords": [
6
6
  "mcp",