doora-mcp 0.1.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 ADDED
@@ -0,0 +1,76 @@
1
+ # doora-mcp
2
+
3
+ Model Context Protocol server for [doora](https://www.doora.to) — let
4
+ Claude Desktop / Cursor / Continue / Zed (or anything else that
5
+ speaks MCP) look up addresses by doora handle.
6
+
7
+ ## Install
8
+
9
+ The server runs via `npx`. No local install needed. Drop this block
10
+ into your MCP client's config and restart:
11
+
12
+ ```jsonc
13
+ // Claude Desktop on macOS:
14
+ // ~/Library/Application Support/Claude/claude_desktop_config.json
15
+ {
16
+ "mcpServers": {
17
+ "doora": {
18
+ "command": "npx",
19
+ "args": ["-y", "doora-mcp"],
20
+ "env": {
21
+ "DOORA_API_KEY": "dk_test_..."
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ You should see "doora" in Claude Desktop's available-tools panel.
29
+
30
+ For Cursor, Continue, Zed, and other clients, see
31
+ **https://www.doora.to/docs/mcp** for per-client config snippets.
32
+
33
+ ## Get an API key
34
+
35
+ You'll need a doora merchant API key in the `DOORA_API_KEY` env
36
+ slot. Two flavours:
37
+
38
+ - **`dk_test_…`** — sandbox key. Resolves only the curated `demo@*`
39
+ handles. Safe to put in a config file shared with collaborators.
40
+ - **`dk_live_…`** — production key, resolves real handles your
41
+ merchant integration is authorised for.
42
+
43
+ Request a key via [the contact form](https://www.doora.to/contact).
44
+
45
+ The MCP server makes no auth check at startup — Claude can start it
46
+ and list its tools, and only sees a friendly "configure DOORA_API_KEY"
47
+ error when you first try to resolve a handle.
48
+
49
+ ## Tools
50
+
51
+ | Tool | What it does | Network |
52
+ |---|---|---|
53
+ | `resolve_handle` | Look up address for a `username@label` handle | ✓ |
54
+ | `encode_digipin` | lat/lng → 12-char DIGIPIN string | — |
55
+ | `decode_digipin` | DIGIPIN → lat/lng centroid | — |
56
+ | `list_demo_handles` | Curated list of test-key-resolvable handles | — |
57
+ | `validate_handle_shape` | Regex-check a handle without an API call | — |
58
+
59
+ Full reference with example prompts and trust model at
60
+ **https://www.doora.to/docs/mcp**.
61
+
62
+ ## Configuration
63
+
64
+ | Env var | Default | Purpose |
65
+ |---|---|---|
66
+ | `DOORA_API_KEY` | *(required)* | Bearer token for resolve calls |
67
+ | `DOORA_BASE_URL` | `https://www.doora.to` | Override for self-hosted deployments |
68
+
69
+ ## Support
70
+
71
+ - **Docs:** https://www.doora.to/docs/mcp
72
+ - **Contact:** https://www.doora.to/contact
73
+
74
+ ## License
75
+
76
+ MIT.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * HTTP client wrapping the doora merchant API.
3
+ *
4
+ * Reads:
5
+ * DOORA_API_KEY required for every tool that hits the server.
6
+ * Format: `dk_test_…` or `dk_live_…`.
7
+ * DOORA_BASE_URL optional, defaults to https://www.doora.to.
8
+ * Override only for staging / local dev.
9
+ *
10
+ * Important: we DO NOT validate DOORA_API_KEY at module load. That
11
+ * would block the MCP server from booting (and listing its tools to
12
+ * Claude Desktop) just because the user hadn't configured the key
13
+ * yet. Instead we surface the missing-key error at tool-invocation
14
+ * time with a clear message pointing at the fix.
15
+ */
16
+ const DEFAULT_BASE_URL = 'https://www.doora.to';
17
+ export class DoraApiError extends Error {
18
+ status;
19
+ body;
20
+ constructor(status, body, message) {
21
+ super(message);
22
+ this.name = 'DoraApiError';
23
+ this.status = status;
24
+ this.body = body;
25
+ }
26
+ }
27
+ /** Throws a tagged error when DOORA_API_KEY is missing. Caller turns
28
+ * this into a friendly tool response. */
29
+ function readApiKey() {
30
+ const key = process.env.DOORA_API_KEY?.trim();
31
+ if (!key) {
32
+ throw new DoraApiError(0, null, 'DOORA_API_KEY environment variable not set. Configure it in your MCP client config (e.g. in claude_desktop_config.json under env). See https://www.doora.to/docs/mcp for setup.');
33
+ }
34
+ return key;
35
+ }
36
+ function baseUrl() {
37
+ const raw = process.env.DOORA_BASE_URL?.trim() || DEFAULT_BASE_URL;
38
+ return raw.replace(/\/+$/, '');
39
+ }
40
+ async function request(path, init = {}) {
41
+ const key = readApiKey();
42
+ const headers = new Headers(init.headers);
43
+ headers.set('authorization', `Bearer ${key}`);
44
+ if (init.body && !headers.has('content-type')) {
45
+ headers.set('content-type', 'application/json');
46
+ }
47
+ // Tag every call so doora can identify MCP traffic in its logs
48
+ // (useful for usage analytics + isolating issues to this client).
49
+ headers.set('user-agent', `doora-mcp/${process.env.npm_package_version ?? 'dev'} (+https://www.doora.to/docs/mcp)`);
50
+ const url = `${baseUrl()}${path}`;
51
+ const res = await fetch(url, { ...init, headers });
52
+ let body = null;
53
+ try {
54
+ body = await res.json();
55
+ }
56
+ catch { /* response wasn't JSON, leave body=null */ }
57
+ if (!res.ok) {
58
+ const msg = (typeof body === 'object' && body && 'error' in body && typeof body.error === 'string')
59
+ ? body.error
60
+ : `HTTP ${res.status} at ${path}`;
61
+ throw new DoraApiError(res.status, body, msg);
62
+ }
63
+ return body;
64
+ }
65
+ export async function resolveHandle(handle) {
66
+ const trimmed = handle.trim().toLowerCase();
67
+ return request(`/api/v1/resolve?handle=${encodeURIComponent(trimmed)}`);
68
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * DIGIPIN encoder / decoder — TypeScript port of India Post's
3
+ * reference implementation, inlined here so the MCP package has no
4
+ * runtime dependency on the main doora monorepo.
5
+ *
6
+ * Mirrors lib/digipin.ts in the main repo exactly. Both files MUST
7
+ * stay in sync; the unit suite at tests/unit/digipin.test.ts pins
8
+ * the shape that both sides must produce. If you change the grid,
9
+ * the bounding box, or the levels here, update the main lib AND
10
+ * re-run the tests there.
11
+ *
12
+ * 10-character output (12 with separators). Each character bisects
13
+ * the bounding box four ways, so level 10 lands inside a ~4m × 4m
14
+ * cell. We only target India — the bounding box is hard-coded.
15
+ */
16
+ const GRID = [
17
+ ['F', 'C', '9', '8'],
18
+ ['J', '3', '2', '7'],
19
+ ['K', '4', '5', '6'],
20
+ ['L', 'M', 'P', 'T'],
21
+ ];
22
+ export const DIGIPIN_BOUNDS = {
23
+ minLat: 2.5,
24
+ maxLat: 38.5,
25
+ minLon: 63.5,
26
+ maxLon: 99.5,
27
+ };
28
+ const LEVELS = 10;
29
+ const CHAR_INDEX = (() => {
30
+ const map = new Map();
31
+ for (let r = 0; r < 4; r++) {
32
+ for (let c = 0; c < 4; c++) {
33
+ map.set(GRID[r][c], [r, c]);
34
+ }
35
+ }
36
+ return map;
37
+ })();
38
+ /** Returns the 12-character DIGIPIN string ("XXX-XXX-XXXX") for a
39
+ * point inside the India bounding box, or null for inputs outside
40
+ * it (NaN, Antarctica, the moon). */
41
+ export function encodeDigipin(lat, lon) {
42
+ if (!Number.isFinite(lat) || !Number.isFinite(lon))
43
+ return null;
44
+ if (lat < DIGIPIN_BOUNDS.minLat || lat > DIGIPIN_BOUNDS.maxLat)
45
+ return null;
46
+ if (lon < DIGIPIN_BOUNDS.minLon || lon > DIGIPIN_BOUNDS.maxLon)
47
+ return null;
48
+ let minLat = DIGIPIN_BOUNDS.minLat;
49
+ let maxLat = DIGIPIN_BOUNDS.maxLat;
50
+ let minLon = DIGIPIN_BOUNDS.minLon;
51
+ let maxLon = DIGIPIN_BOUNDS.maxLon;
52
+ let pin = '';
53
+ for (let level = 1; level <= LEVELS; level++) {
54
+ const latDiv = (maxLat - minLat) / 4;
55
+ const lonDiv = (maxLon - minLon) / 4;
56
+ let row = 3 - Math.floor((lat - minLat) / latDiv);
57
+ let col = Math.floor((lon - minLon) / lonDiv);
58
+ if (row < 0)
59
+ row = 0;
60
+ else if (row > 3)
61
+ row = 3;
62
+ if (col < 0)
63
+ col = 0;
64
+ else if (col > 3)
65
+ col = 3;
66
+ pin += GRID[row][col];
67
+ if (level === 3 || level === 6)
68
+ pin += '-';
69
+ const newMaxLat = minLat + latDiv * (4 - row);
70
+ const newMinLat = minLat + latDiv * (3 - row);
71
+ maxLat = newMaxLat;
72
+ minLat = newMinLat;
73
+ const newMinLon = minLon + lonDiv * col;
74
+ minLon = newMinLon;
75
+ maxLon = newMinLon + lonDiv;
76
+ }
77
+ return pin;
78
+ }
79
+ /** Decodes a 10-char DIGIPIN string (dashes optional, case-
80
+ * insensitive) back to the centroid lat/lng of its level-10 cell.
81
+ * Throws RangeError on malformed input — callers catch + repackage. */
82
+ export function decodeDigipin(digipin) {
83
+ const pin = digipin.toUpperCase().replace(/-/g, '');
84
+ if (pin.length !== 10) {
85
+ throw new RangeError(`DIGIPIN must be 10 chars (got ${pin.length})`);
86
+ }
87
+ let minLat = DIGIPIN_BOUNDS.minLat;
88
+ let maxLat = DIGIPIN_BOUNDS.maxLat;
89
+ let minLon = DIGIPIN_BOUNDS.minLon;
90
+ let maxLon = DIGIPIN_BOUNDS.maxLon;
91
+ for (let i = 0; i < LEVELS; i++) {
92
+ const idx = CHAR_INDEX.get(pin[i]);
93
+ if (!idx)
94
+ throw new RangeError(`Invalid DIGIPIN character "${pin[i]}"`);
95
+ const [ri, ci] = idx;
96
+ const latDiv = (maxLat - minLat) / 4;
97
+ const lonDiv = (maxLon - minLon) / 4;
98
+ minLat = maxLat - latDiv * (ri + 1);
99
+ maxLat = maxLat - latDiv * ri;
100
+ minLon = minLon + lonDiv * ci;
101
+ maxLon = minLon + lonDiv;
102
+ }
103
+ return {
104
+ lat: (minLat + maxLat) / 2,
105
+ lng: (minLon + maxLon) / 2,
106
+ };
107
+ }
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * doora-mcp — Model Context Protocol server for doora.
4
+ *
5
+ * Lets an LLM agent (Claude Desktop, Cursor, Continue, etc.) look up
6
+ * addresses by doora handle, decode + encode DIGIPINs, and discover
7
+ * the demo handles available to test against.
8
+ *
9
+ * Install + run:
10
+ *
11
+ * $ npx -y doora-mcp (Claude Desktop launches us this way)
12
+ *
13
+ * Configure in Claude Desktop (~/Library/Application Support/Claude/
14
+ * claude_desktop_config.json on macOS):
15
+ *
16
+ * {
17
+ * "mcpServers": {
18
+ * "doora": {
19
+ * "command": "npx",
20
+ * "args": ["-y", "doora-mcp"],
21
+ * "env": { "DOORA_API_KEY": "dk_test_..." }
22
+ * }
23
+ * }
24
+ * }
25
+ *
26
+ * Architecture notes:
27
+ *
28
+ * - stdio transport only (v1). Claude Desktop + Cursor + Continue
29
+ * all support stdio out of the box. SSE/HTTP transport adds
30
+ * hosting overhead we don't need yet.
31
+ *
32
+ * - Tools are stateless. Each call is independent — no session,
33
+ * no cookies, just an API key in the bearer header. That keeps
34
+ * the surface boring + auditable.
35
+ *
36
+ * - We DO NOT probe DOORA_API_KEY at startup. Claude Desktop
37
+ * happily starts servers whose env isn't configured yet; the
38
+ * friendly "missing key" message lands at first tool call.
39
+ *
40
+ * - Every tool returns text content (not embedded JSON) so the
41
+ * LLM has the easiest time reading what came back. Pretty-
42
+ * printed JSON in a code block is the lingua franca.
43
+ */
44
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
45
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
46
+ import { z } from 'zod';
47
+ import { encodeDigipin, decodeDigipin } from './digipin.js';
48
+ import { DoraApiError, resolveHandle } from './api-client.js';
49
+ const HANDLE_PATTERN = /^([a-z0-9-]{3,24})@([a-z0-9-]{2,16})$/i;
50
+ const server = new McpServer({
51
+ name: 'doora',
52
+ version: '0.1.0',
53
+ }, {
54
+ capabilities: { tools: {} },
55
+ instructions: `doora exposes India-aware addresses behind memorable handles like \`praneeth@home\`. ` +
56
+ `Use these tools whenever the user asks to look up an address by handle, ` +
57
+ `encode coordinates to a DIGIPIN, decode a DIGIPIN to coordinates, ` +
58
+ `or check whether a string is a valid doora handle. ` +
59
+ `Call list_demo_handles first if you need a sandbox handle to demonstrate against.`,
60
+ });
61
+ // ─── Tool 1: resolve_handle ──────────────────────────────────────────
62
+ server.registerTool('resolve_handle', {
63
+ title: 'Resolve doora handle to address',
64
+ description: 'Look up the full delivery address for a doora handle. Returns coordinates, ' +
65
+ 'DIGIPIN, building/floor/unit fields the owner has shared, verification method, ' +
66
+ 'and an audit id. Use this when the user mentions any string in `username@label` ' +
67
+ 'shape (e.g. "praneeth@home", "demo@office") and wants the underlying address. ' +
68
+ 'Requires the DOORA_API_KEY environment variable; test keys can only resolve ' +
69
+ 'handles owned by the configured demo user — call list_demo_handles to see those.',
70
+ inputSchema: {
71
+ handle: z.string()
72
+ .describe('Doora handle in the form `username@label`. Case-insensitive; will be lowercased before lookup.'),
73
+ },
74
+ }, async ({ handle }) => {
75
+ try {
76
+ const result = await resolveHandle(handle);
77
+ return {
78
+ content: [
79
+ { type: 'text', text: formatResolveResult(result) },
80
+ ],
81
+ };
82
+ }
83
+ catch (e) {
84
+ return errorContent(e, `Could not resolve "${handle}"`);
85
+ }
86
+ });
87
+ // ─── Tool 2: encode_digipin ──────────────────────────────────────────
88
+ server.registerTool('encode_digipin', {
89
+ title: 'Encode lat/lng to DIGIPIN',
90
+ description: 'Convert a latitude+longitude pair (in India) to a DIGIPIN — India Post\'s open ' +
91
+ 'national geocode. Output is a 10-character (12 with separators) string identifying ' +
92
+ 'a ~4m × 4m cell. Pure local computation; no network call, no API key needed. ' +
93
+ 'Returns null for coordinates outside India.',
94
+ inputSchema: {
95
+ lat: z.number().min(2.5).max(38.5).describe('Latitude in degrees, within India\'s bounding box.'),
96
+ lng: z.number().min(63.5).max(99.5).describe('Longitude in degrees, within India\'s bounding box.'),
97
+ },
98
+ }, async ({ lat, lng }) => {
99
+ const pin = encodeDigipin(lat, lng);
100
+ if (!pin) {
101
+ return {
102
+ content: [{ type: 'text', text: `Coordinates (${lat}, ${lng}) are outside the India bounding box; no DIGIPIN exists.` }],
103
+ isError: true,
104
+ };
105
+ }
106
+ return {
107
+ content: [
108
+ { type: 'text', text: `DIGIPIN for (${lat.toFixed(6)}, ${lng.toFixed(6)}):\n\n\`${pin}\`\n\nResolution: ~4m × 4m cell.` },
109
+ ],
110
+ };
111
+ });
112
+ // ─── Tool 3: decode_digipin ──────────────────────────────────────────
113
+ server.registerTool('decode_digipin', {
114
+ title: 'Decode DIGIPIN to lat/lng',
115
+ description: 'Convert a DIGIPIN (10-char India Post geocode) back to latitude+longitude. ' +
116
+ 'Dashes and case are tolerated (`422-36L-P8FM`, `42236lp8fm` both work). The ' +
117
+ 'returned point is the centroid of the level-10 cell — accurate to within ~2m of ' +
118
+ 'the original encoded point. Pure local computation; no network call.',
119
+ inputSchema: {
120
+ digipin: z.string().min(10).describe('A 10-character DIGIPIN string. Dashes and case are ignored.'),
121
+ },
122
+ }, async ({ digipin }) => {
123
+ try {
124
+ const { lat, lng } = decodeDigipin(digipin);
125
+ return {
126
+ content: [
127
+ { type: 'text', text: `\`${digipin}\` decodes to:\n\n lat: ${lat}\n lng: ${lng}\n\nOpen in Google Maps: https://maps.google.com/?q=${lat},${lng}` },
128
+ ],
129
+ };
130
+ }
131
+ catch (e) {
132
+ const msg = e instanceof Error ? e.message : String(e);
133
+ return {
134
+ content: [{ type: 'text', text: `Failed to decode "${digipin}": ${msg}` }],
135
+ isError: true,
136
+ };
137
+ }
138
+ });
139
+ // ─── Tool 4: list_demo_handles ───────────────────────────────────────
140
+ server.registerTool('list_demo_handles', {
141
+ title: 'List demo handles available to test keys',
142
+ description: 'Return the curated set of handles a `dk_test_` API key can resolve. Use this ' +
143
+ 'when you need a known-good handle to demonstrate the resolve_handle tool, OR ' +
144
+ 'when a resolve attempt returned 404 against a test key and you want to suggest ' +
145
+ 'an alternative the user can try. Static list — no network call.',
146
+ inputSchema: {},
147
+ }, async () => {
148
+ const handles = [
149
+ { handle: 'demo@home', note: 'Happy path: apartment in Hyderabad. All address fields visible, walk_and_lock verified to ~4m.' },
150
+ { handle: 'demo@office', note: 'Office building. building + floor visible; unit hidden (returns null).' },
151
+ { handle: 'demo@gate', note: 'Manual-satellite verification, larger accuracy. Tests the "low-precision pin" branch.' },
152
+ { handle: 'demo@private', note: 'Owner set privacy_mode=private. Returns 404 — verify your handler treats this the same as "doesn\'t exist".' },
153
+ ];
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: 'Demo handles available to test keys:\n\n' + handles.map((h) => `• \`${h.handle}\` — ${h.note}`).join('\n') +
159
+ '\n\nAny other handle (or a live-only handle resolved with a test key) returns 404 by design — see https://www.doora.to/docs/api#demo-handles for the full rationale.',
160
+ },
161
+ ],
162
+ };
163
+ });
164
+ // ─── Tool 5: validate_handle_shape ───────────────────────────────────
165
+ server.registerTool('validate_handle_shape', {
166
+ title: 'Validate doora handle shape',
167
+ description: 'Check whether a string is a structurally valid doora handle (`username@label`) ' +
168
+ 'without hitting the API. Useful before calling resolve_handle to surface a clear ' +
169
+ 'error message to the user. Pure regex; no network call, no API key.',
170
+ inputSchema: {
171
+ input: z.string().describe('The string to validate. Will be trimmed + lowercased before checking.'),
172
+ },
173
+ }, async ({ input }) => {
174
+ const normalized = input.trim().toLowerCase();
175
+ const m = normalized.match(HANDLE_PATTERN);
176
+ if (!m) {
177
+ return {
178
+ content: [
179
+ {
180
+ type: 'text',
181
+ text: `"${input}" is NOT a valid doora handle.\n\nA handle has the form \`username@label\`:\n • username: 3-24 chars, lowercase a-z, 0-9, hyphen\n • label: 2-16 chars, lowercase a-z, 0-9, hyphen\n • exactly one \`@\` between them\n\nExamples that ARE valid: praneeth@home, demo@office, 0x-viking@flat-2.`,
182
+ },
183
+ ],
184
+ };
185
+ }
186
+ const [, username, label] = m;
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: `"${input}" IS a valid doora handle.\n\n normalized: ${normalized}\n username: ${username}\n label: ${label}\n\nCall resolve_handle with the normalized form to look up the address.`,
192
+ },
193
+ ],
194
+ };
195
+ });
196
+ // ─── Helpers ────────────────────────────────────────────────────────
197
+ function formatResolveResult(r) {
198
+ // The address payload is most readable when pretty-printed as JSON
199
+ // inside a code block. Mode + audit id surface above the block so
200
+ // the LLM can mention them without re-parsing JSON.
201
+ return [
202
+ `**${r.handle}** resolved (mode: ${r.mode}, audit: ${r.audit_id.slice(0, 8)}…)`,
203
+ '',
204
+ '```json',
205
+ JSON.stringify(r, null, 2),
206
+ '```',
207
+ ].join('\n');
208
+ }
209
+ function errorContent(e, prefix) {
210
+ if (e instanceof DoraApiError) {
211
+ const status = e.status === 0 ? 'setup' : `${e.status}`;
212
+ return {
213
+ content: [{ type: 'text', text: `${prefix}: ${e.message} (${status})` }],
214
+ isError: true,
215
+ };
216
+ }
217
+ const msg = e instanceof Error ? e.message : String(e);
218
+ return {
219
+ content: [{ type: 'text', text: `${prefix}: ${msg}` }],
220
+ isError: true,
221
+ };
222
+ }
223
+ // ─── Start ──────────────────────────────────────────────────────────
224
+ async function main() {
225
+ const transport = new StdioServerTransport();
226
+ await server.connect(transport);
227
+ // Note: do NOT console.log here — stdout is the MCP transport
228
+ // channel. Any stray output corrupts the JSON-RPC stream. Use
229
+ // console.error for diagnostics (stderr is safe to log to).
230
+ console.error(`[doora-mcp] connected via stdio. base url: ${process.env.DOORA_BASE_URL || 'https://www.doora.to'}`);
231
+ }
232
+ main().catch((err) => {
233
+ console.error('[doora-mcp] fatal:', err);
234
+ process.exit(1);
235
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "doora-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for doora — let Claude / Cursor / Continue look up addresses by doora handle.",
5
+ "keywords": [
6
+ "mcp",
7
+ "modelcontextprotocol",
8
+ "doora",
9
+ "address",
10
+ "digipin",
11
+ "india",
12
+ "claude"
13
+ ],
14
+ "homepage": "https://www.doora.to/docs/mcp",
15
+ "bugs": "https://www.doora.to/contact",
16
+ "license": "MIT",
17
+ "author": "doora",
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "bin": {
24
+ "doora-mcp": "dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "dev": "tsx src/index.ts",
33
+ "prepublishOnly": "npm run build",
34
+ "smoke": "node scripts/smoke.mjs",
35
+ "start": "node dist/index.js"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.4"
39
+ },
40
+ "devDependencies": {
41
+ "tsx": "^4.19.2",
42
+ "typescript": "^5.7.0",
43
+ "@types/node": "^22.10.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }