@nodatachat/mcp 0.4.2 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +151 -116
  2. package/dist/index.js +453 -42
  3. package/package.json +43 -38
package/README.md CHANGED
@@ -1,116 +1,151 @@
1
- # @nodatachat/mcp
2
-
3
- MCP (Model Context Protocol) server for **governing data given to AI agents** — plus NoData's Blind Relay (encrypt / decrypt / deliver / evidence).
4
-
5
- ## Two modes — the credential decides what the server can do
6
-
7
- **Agent mode (`--grant-token ndca-...`)** — *permission management for language models.*
8
- You hand an AI agent a grant token; this server exposes tools hard-scoped to that grant: `nodata_decide` (may I reach this? — decision + proof, no data), `nodata_retrieve` (authorized context, filtered to what you may see), `nodata_read` (read governed rows), and `nodata_use` (secret-blind invoke). The agent may only request columns its grant allows — **denied columns never decrypt server-side**, and every action emits a cryptographic receipt. Point any MCP client (Claude, an agent runtime) at it and it physically cannot exceed its claims.
9
-
10
- The plan-then-act loop: **`nodata_decide`** to check reachability (allow / degrade / deny + access-distance cost), then **`nodata_retrieve`** to pull only the authorized context into your prompt.
11
-
12
- ```bash
13
- claude mcp add nodata -- npx @nodatachat/mcp --grant-token ndca-YOUR_GRANT
14
- ```
15
-
16
- **Admin mode (`--api-key sk_live_...`)** — manage governance from inside any MCP client: register your tables, issue grants, revoke them, and use blind relay.
17
-
18
- ```bash
19
- claude mcp add nodata -- npx @nodatachat/mcp --api-key YOUR_API_KEY
20
- ```
21
-
22
- ## Installation
23
-
24
- ```bash
25
- npm install @nodatachat/mcp
26
- ```
27
-
28
- Or run directly with npx:
29
-
30
- ```bash
31
- npx @nodatachat/mcp --api-key sk_live_your_key_here
32
- ```
33
-
34
- ## Claude Code Setup
35
-
36
- Add the server to Claude Code in one command:
37
-
38
- ```bash
39
- claude mcp add nodata -- npx @nodatachat/mcp --api-key YOUR_API_KEY
40
- ```
41
-
42
- On **Windows**, native (non-WSL) `npx` needs the `cmd /c` wrapper:
43
-
44
- ```cmd
45
- claude mcp add nodata -- cmd /c npx @nodatachat/mcp --api-key YOUR_API_KEY
46
- ```
47
-
48
- After changing the server's config (e.g. adding a credential), **restart Claude Code** — the running server process keeps its old arguments until the session restarts.
49
-
50
- Or with a custom base URL (for self-hosted or staging):
51
-
52
- ```bash
53
- claude mcp add nodata -- npx @nodatachat/mcp --api-key YOUR_API_KEY --base-url https://staging.nodatacapsule.com
54
- ```
55
-
56
- ## Configuration
57
-
58
- | Option | Flag | Env Var | Default |
59
- |--------|------|---------|---------|
60
- | API Key (admin) | `--api-key` | `NODATA_API_KEY` | — |
61
- | Grant token (agent) | `--grant-token` | `NODATA_GRANT_TOKEN` | — |
62
- | Base URL | `--base-url` | `NODATA_BASE_URL` | `https://www.nodatacapsule.com` |
63
-
64
- API keys use the format `sk_live_…` (get one free at [/developers/get-started](https://www.nodatacapsule.com/developers/get-started)); grant tokens are `ndca-…` (mint via `POST /api/v1/governance/grant` or the `nodata_grant` tool).
65
-
66
- **No credential? Setup mode.** Started without a credential, the server still connects and exposes a single `nodata_get_started` tool that walks you (or your AI) through getting a key and reconnecting — it never fails the MCP handshake. Data tools stay disabled until a credential is provided. `--help` and `--version` are also available.
67
-
68
- ## Available Tools
69
-
70
- **Agent mode** (`--grant-token`):
71
-
72
- | Tool | Description |
73
- |------|-------------|
74
- | `nodata_decide` | Ask whether the agent MAY reach a resource, without reading it. Returns allow / degrade (reachable subset) / deny + an access-distance cost + a signed proof. No data. Plan before you read. |
75
- | `nodata_retrieve` | Prompt-ready authorized context, filtered to exactly the allowed columns (denied columns never decrypt), with an accounting of what was withheld + a signed proof. |
76
- | `nodata_read` | Read governed data within this agent's grant. Denied columns never decrypt; every read emits a receipt. Hard-scoped to the grant token. |
77
- | `nodata_use` | Invoke a pre-registered secret-blind capability; the Capsule injects the sealed credential server-side and returns only the result. The secret never reaches the agent. |
78
-
79
- **Admin mode** (`--api-key`):
80
-
81
- | Tool | Description |
82
- |------|-------------|
83
- | `nodata_register_table` | Register one of your tables as AI-agent-readable ("connect your data"). |
84
- | `nodata_grant` | Issue an agent a claim-scoped grant; returns a grant_token (shown once). |
85
- | `nodata_revoke` | Revoke a grant by handle or jti. |
86
- | `nodata_encrypt` | Encrypt a field (AES-256-GCM). Returns ciphertext for you to store. NoData stores nothing. |
87
- | `nodata_decrypt` | Decrypt ciphertext back to plaintext. |
88
- | `nodata_deliver` | Create a burn-after-read secure link with OTP. |
89
- | `nodata_evidence` | Retrieve audit trail (metadata only). |
90
-
91
- ## Example Usage in Claude Code
92
-
93
- Once the MCP server is added, you can use natural language:
94
-
95
- - "Encrypt my database password using NoData"
96
- - "Create a secure link to send this API key to the DevOps team"
97
- - "Show me the audit trail for the last 7 days"
98
- - "Decrypt this ciphertext: aes256gcm:v1:..."
99
-
100
- ## Development
101
-
102
- ```bash
103
- cd packages/mcp-server
104
- npm install
105
- npm run dev -- --api-key sk_live_test_key
106
- ```
107
-
108
- Build for production:
109
-
110
- ```bash
111
- npm run build
112
- ```
113
-
114
- ## License
115
-
116
- MIT
1
+ # @nodatachat/mcp
2
+
3
+ **Give an AI agent real power over your data — without the power to leak it.**
4
+
5
+ One line connects any MCP client (Claude Code, an agent runtime) to NoData's governed‑access layer. Your agent asks *"may I reach this?"* and only ever sees the columns you granted. Denied columns are **never decrypted on the server** — there is nothing to leak — and every access leaves a signed, verifiable receipt.
6
+
7
+ ```bash
8
+ claude mcp add nodata -- npx @nodatachat/mcp
9
+ ```
10
+
11
+ That's it — **no flags**. Self‑serve: registering opens an instant free **sandbox key** (no approval wall) and a **12‑word recovery phrase only you hold**. No card. **10,000 governed decisions per month free**, then $0.25 per 1,000.
12
+
13
+ > **The one idea:** *API access ≠ data access.* A successful call is a request for a **decision**, not a handoff of data. **`no key = no plaintext`**, and every decision carries a proof.
14
+
15
+ ---
16
+
17
+ ## Get started in one command
18
+
19
+ Run the line above with no credential. The server still connects (it never fails the MCP handshake) and exposes two setup tools:
20
+
21
+ - **`nodata_get_started`** — call it first. It explains what you get and the single next step.
22
+ - **`nodata_connect`** — register once, autoload forever:
23
+ 1. `nodata_connect` (no args) → opens the free signup in your browser (work email + company, ~30s). Your **12‑word recovery phrase** is generated **in your browser — NoData never sees it**. The page shows your org key **once**.
24
+ 2. `nodata_connect` with `api_key: "<that key>"` → saved to `~/.nodata/credentials.json` (chmod 600).
25
+ 3. Restart, or run `/mcp` → every later start loads your identity with nothing to retype. *Registration ≠ login.*
26
+
27
+ Keep the 12‑word phrase somewhere safe — it is the only way back into your org, and NoData cannot reissue it.
28
+
29
+ ---
30
+
31
+ ## One journey, five verbs, two ways in
32
+
33
+ The whole product is one primitive shown five ways — **Classify · Protect · Route · Retrieve · Prove** — entered from **a table** (govern the DB columns an agent reads) **or a folder/document** (seal into a Capsule). Same journey, same proof.
34
+
35
+ | Verb | What you do | Tool |
36
+ |------|-------------|------|
37
+ | **Classify** | connect a table (or seal a document) | `nodata_register_table` |
38
+ | **Protect** | grant an agent only the columns you name | `nodata_grant` → `ndca-` token |
39
+ | **Route** | *"may this agent reach X?"* — plan, no data moves | `nodata_decide` |
40
+ | **Retrieve** | pull only the authorized columns into the prompt | `nodata_retrieve` / `nodata_read` |
41
+ | **Prove** | a signed receipt on every read **and** every deny | `nodata_evidence` · `/decisions` |
42
+
43
+ ---
44
+
45
+ ## Try it — the "wow" in four calls
46
+
47
+ After `nodata_connect`, ask your agent in plain language (*"grant a support bot first_name and last_name on agent_demo_records, then read it"*), or run the raw API with your key (`export NDP=ndp_test_…`):
48
+
49
+ ```bash
50
+ # 1 · grant an agent two columns of the built-in demo table (auto-seeds sample rows)
51
+ curl -s -X POST https://www.nodatacapsule.com/api/v1/governance/grant \
52
+ -H "Authorization: Bearer $NDP" -H 'content-type: application/json' \
53
+ -d '{"agent":"support-copilot","table":"agent_demo_records","columns":["first_name","last_name"]}'
54
+ # → { "grant_token": "ndca-…", "handle": "…" }
55
+
56
+ # 2 · the agent reads what it's allowed — RELEASED
57
+ curl -s -X POST https://www.nodatacapsule.com/api/agents/<handle>/read \
58
+ -H "Authorization: Bearer ndca-…" -H 'content-type: application/json' \
59
+ -d '{"table":"agent_demo_records","columns":["first_name","last_name"],"limit":5}'
60
+ # → 200 { "rows": [ { "first_name": "…", "last_name": "…" }, … ] }
61
+
62
+ # 3 · the agent asks for a column it was NOT granted — WITHHELD
63
+ curl -s -X POST https://www.nodatacapsule.com/api/agents/<handle>/read \
64
+ -H "Authorization: Bearer ndca-…" -H 'content-type: application/json' \
65
+ -d '{"table":"agent_demo_records","columns":["email"]}'
66
+ # → 403 { "error": "scope_violation", "denied_columns": ["email"] } ← the key for email is never derived
67
+
68
+ # 4 · prove it — signed receipts for the allow AND the deny, verifiable without the data:
69
+ # https://www.nodatacapsule.com/decisions
70
+ ```
71
+
72
+ `email` came back **absent, not blanked** — there is no decrypt‑all‑then‑filter path. That is *"you decide what the AI sees,"* as math, not a promise.
73
+
74
+ No terminal? Same idea in the browser, zero install: **https://www.nodatacapsule.com/ai-folder** — drop a folder, see exactly what an AI would be allowed to read (nothing uploads).
75
+
76
+ ---
77
+
78
+ ## Two modes — the credential decides what the server can do
79
+
80
+ **Agent mode (`--grant-token ndca-…`)** — *permission management for language models.* Hand an AI agent a grant token; the server exposes only tools hard‑scoped to that grant. The agent may request **only** the columns its grant allows — denied columns never decrypt server‑side, and every action emits a receipt. Point any MCP client at it and it **physically cannot exceed its claims**.
81
+
82
+ ```bash
83
+ claude mcp add nodata -- npx @nodatachat/mcp --grant-token ndca-YOUR_GRANT
84
+ ```
85
+
86
+ **Admin mode (`--api-key ndp_… / sk_live_…`)** — the owner seat: register tables, issue and revoke grants, and use blind relay (encrypt / decrypt / deliver / evidence) from inside any MCP client.
87
+
88
+ ```bash
89
+ claude mcp add nodata -- npx @nodatachat/mcp --api-key YOUR_API_KEY
90
+ ```
91
+
92
+ Credential precedence: `--api-key` / `--grant-token` › `NODATA_API_KEY` / `NODATA_GRANT_TOKEN` › the saved `~/.nodata` file. An explicit flag or env always wins.
93
+
94
+ On **Windows**, native (non‑WSL) `npx` needs the `cmd /c` wrapper: `claude mcp add nodata -- cmd /c npx @nodatachat/mcp …`. After changing the credential, **restart** — the running server keeps its old arguments.
95
+
96
+ ---
97
+
98
+ ## Available tools
99
+
100
+ **Agent mode** (`--grant-token`):
101
+
102
+ | Tool | What it does |
103
+ |------|--------------|
104
+ | `nodata_decide` | May the agent reach a resource? allow / degrade (reachable subset) / deny + an access‑distance cost + a signed proof. No data. Plan before you read. |
105
+ | `nodata_retrieve` | Prompt‑ready authorized context, filtered to allowed columns, with an accounting of what was withheld + a proof. |
106
+ | `nodata_read` | Read governed rows within the grant; denied columns never decrypt; every read is receipted. |
107
+ | `nodata_use` | Invoke a pre‑registered secret‑blind capability — the Capsule injects the sealed credential server‑side and returns only the result. The secret never reaches the agent. |
108
+
109
+ **Admin mode** (`--api-key`):
110
+
111
+ | Tool | What it does |
112
+ |------|--------------|
113
+ | `nodata_register_table` | Register one of your tables as AI‑agent‑readable ("connect your data"). |
114
+ | `nodata_grant` | Issue an agent a claim‑scoped grant; returns a `grant_token` (shown once). |
115
+ | `nodata_revoke` | Revoke a grant by handle or jti — access stops at once. |
116
+ | `nodata_encrypt` / `nodata_decrypt` | Field‑level AES‑256‑GCM; NoData stores nothing of the plaintext. |
117
+ | `nodata_deliver` | Create a burn‑after‑read secure link with OTP. |
118
+ | `nodata_evidence` | Retrieve the audit trail — metadata only, never values. |
119
+
120
+ ---
121
+
122
+ ## Security & privacy model
123
+
124
+ - **Content‑blind.** For governed reads, denied columns are **never decrypted on the server** — the key release is scoped to the grant, not "we promise not to look."
125
+ - **Every action is a receipt.** Reads, grants, revokes and denials mint a signed, hash‑chained decision receipt (Ed25519, publicly verifiable), written **before** the release it authorizes — if the receipt can't be written, nothing is served.
126
+ - **Structural isolation.** A key is bound to its org; every governance query is scoped to that tenant, so a key cannot aim at another org's data.
127
+ - **The honest boundary.** This governs data the agent reaches **through the Capsule**. It does not sandbox a process already executing as you on your own machine.
128
+
129
+ ---
130
+
131
+ ## Configuration
132
+
133
+ | Option | Flag | Env var | Default |
134
+ |--------|------|---------|---------|
135
+ | API key (admin) | `--api-key` | `NODATA_API_KEY` | — |
136
+ | Grant token (agent) | `--grant-token` | `NODATA_GRANT_TOKEN` | — |
137
+ | Base URL | `--base-url` | `NODATA_BASE_URL` | `https://www.nodatacapsule.com` |
138
+
139
+ Get a key free at **https://www.nodatacapsule.com/capsule-api/register** (issues `ndp_test_…` instantly). `--help` and `--version` are also available.
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ cd packages/mcp-server
145
+ npm install
146
+ npm run dev -- --api-key ndp_test_your_key # or: npm run build
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
package/dist/index.js CHANGED
@@ -31,7 +31,61 @@
31
31
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
32
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
33
  import { z } from "zod";
34
- const VERSION = "0.4.2";
34
+ import { homedir, hostname } from "node:os";
35
+ import { join } from "node:path";
36
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
37
+ import { spawn } from "node:child_process";
38
+ import { generateKeyPairSync, createPrivateKey, sign, randomBytes } from "node:crypto";
39
+ const VERSION = "0.8.0";
40
+ // ---------------------------------------------------------------------------
41
+ // Local credential store — "register once, autoload forever"
42
+ // ---------------------------------------------------------------------------
43
+ // This is the apply-side of "registration ≠ login": you register ONCE (in the
44
+ // browser, where the recovery phrase + email verify happen), nodata_connect
45
+ // saves an identity here, and every later start reads it back — no re-register,
46
+ // no re-typing the launch command. The file is chmod 600.
47
+ //
48
+ // Two identity shapes live here:
49
+ // · device — a DEVICE PRIVATE KEY (the preferred, hardened path). No reusable
50
+ // bearer is stored; each start proves possession by signing a fresh nonce
51
+ // (POST /api/v1/attest) and gets a 15-min token. A stolen file is a
52
+ // revocable device key, not a replayable credential.
53
+ // · api_key — the org key at rest (the pre-attest fallback). Still supported
54
+ // for a backend that predates the attest endpoints; it is the standard
55
+ // CLI-login posture (gh/npm/vercel store a token the same way).
56
+ // nodata_connect writes `device` when enrollment succeeds and falls back to
57
+ // `api_key` otherwise. See docs/ATTEST-FABRIC-IDENTITY-SPEC.md.
58
+ const CRED_DIR = join(homedir(), ".nodata");
59
+ const CRED_FILE = join(CRED_DIR, "credentials.json");
60
+ function readStoredCredential() {
61
+ try {
62
+ if (!existsSync(CRED_FILE))
63
+ return null;
64
+ const parsed = JSON.parse(readFileSync(CRED_FILE, "utf8"));
65
+ if (!parsed)
66
+ return null;
67
+ const hasDevice = parsed.device && typeof parsed.device.private_key_pkcs8_b64 === "string";
68
+ const hasKey = typeof parsed.api_key === "string" && parsed.api_key;
69
+ return hasDevice || hasKey ? parsed : null;
70
+ }
71
+ catch {
72
+ // A corrupt or unreadable store must never crash startup — fall through to
73
+ // setup mode, where nodata_connect can rewrite it.
74
+ return null;
75
+ }
76
+ }
77
+ function writeStoredCredential(cred) {
78
+ mkdirSync(CRED_DIR, { recursive: true });
79
+ const payload = JSON.stringify({ ...cred, saved_at: new Date().toISOString() }, null, 2);
80
+ writeFileSync(CRED_FILE, payload, { mode: 0o600 });
81
+ // writeFileSync's mode is ignored if the file already existed; enforce it.
82
+ try {
83
+ chmodSync(CRED_FILE, 0o600);
84
+ }
85
+ catch {
86
+ /* platforms without POSIX perms (Windows) — best effort */
87
+ }
88
+ }
35
89
  const USAGE = `nodata-mcp v${VERSION} — NoData MCP server\n\n` +
36
90
  " ADMIN mode: nodata-mcp --api-key sk_live_...\n" +
37
91
  " AGENT mode: nodata-mcp --grant-token ndca-...\n" +
@@ -67,14 +121,42 @@ function parseArgs(argv) {
67
121
  console.error(`nodata-mcp: unknown option '${argv[i]}' (see --help)`);
68
122
  }
69
123
  }
70
- if (!apiKey)
71
- apiKey = process.env.NODATA_API_KEY ?? "";
72
- if (!grantToken)
73
- grantToken = process.env.NODATA_GRANT_TOKEN ?? "";
124
+ let credentialSource = apiKey || grantToken ? "flag" : "none";
125
+ if (!apiKey && process.env.NODATA_API_KEY) {
126
+ apiKey = process.env.NODATA_API_KEY;
127
+ credentialSource = "env";
128
+ }
129
+ if (!grantToken && process.env.NODATA_GRANT_TOKEN) {
130
+ grantToken = process.env.NODATA_GRANT_TOKEN;
131
+ if (credentialSource === "none")
132
+ credentialSource = "env";
133
+ }
74
134
  if (process.env.NODATA_BASE_URL)
75
135
  baseUrl = process.env.NODATA_BASE_URL;
136
+ // Last resort: what nodata_connect saved. An explicit flag or env always wins,
137
+ // so this only fires on a bare `npx @nodatachat/mcp` — the "autoload forever"
138
+ // path after a one-time connect. A stored DEVICE (attest) is preferred over a
139
+ // stored api_key.
140
+ let device;
141
+ if (!apiKey && !grantToken) {
142
+ const stored = readStoredCredential();
143
+ if (stored) {
144
+ // Honor a stored base_url only if nothing more explicit set one.
145
+ if (!process.env.NODATA_BASE_URL && baseUrl === "https://www.nodatacapsule.com" && stored.base_url) {
146
+ baseUrl = stored.base_url;
147
+ }
148
+ if (stored.device) {
149
+ device = stored.device;
150
+ credentialSource = "stored";
151
+ }
152
+ else if (stored.api_key) {
153
+ apiKey = stored.api_key;
154
+ credentialSource = "stored";
155
+ }
156
+ }
157
+ }
76
158
  baseUrl = baseUrl.replace(/\/+$/, "");
77
- return { apiKey, grantToken, baseUrl };
159
+ return { apiKey, grantToken, baseUrl, credentialSource, device };
78
160
  }
79
161
  // Without a deadline a dead network turns a tool call into an indefinite hang
80
162
  // inside the MCP client; 30s comfortably covers a slow decide/retrieve.
@@ -105,17 +187,35 @@ async function doFetch(url, init) {
105
187
  }
106
188
  return { ok: res.ok, status: res.status, body: parsed };
107
189
  }
190
+ async function resolveBearer(bearer) {
191
+ if (typeof bearer === "string")
192
+ return { token: bearer };
193
+ try {
194
+ return { token: await bearer() };
195
+ }
196
+ catch (err) {
197
+ return {
198
+ error: { ok: false, status: 0, body: { error: `Could not obtain an access token: ${err instanceof Error ? err.message : String(err)}` } },
199
+ };
200
+ }
201
+ }
108
202
  async function apiPost(baseUrl, bearer, path, body) {
203
+ const r = await resolveBearer(bearer);
204
+ if ("error" in r)
205
+ return r.error;
109
206
  return doFetch(`${baseUrl}${path}`, {
110
207
  method: "POST",
111
208
  headers: {
112
209
  "Content-Type": "application/json",
113
- Authorization: `Bearer ${bearer}`,
210
+ Authorization: `Bearer ${r.token}`,
114
211
  },
115
212
  body: JSON.stringify(body),
116
213
  });
117
214
  }
118
215
  async function apiGet(baseUrl, bearer, path, params) {
216
+ const r = await resolveBearer(bearer);
217
+ if ("error" in r)
218
+ return r.error;
119
219
  const url = new URL(`${baseUrl}${path}`);
120
220
  for (const [key, val] of Object.entries(params)) {
121
221
  if (val !== undefined && val !== "")
@@ -123,7 +223,7 @@ async function apiGet(baseUrl, bearer, path, params) {
123
223
  }
124
224
  return doFetch(url.toString(), {
125
225
  method: "GET",
126
- headers: { Authorization: `Bearer ${bearer}` },
226
+ headers: { Authorization: `Bearer ${r.token}` },
127
227
  });
128
228
  }
129
229
  function formatResult(resp) {
@@ -151,6 +251,74 @@ function handleFromGrant(token) {
151
251
  }
152
252
  }
153
253
  // ---------------------------------------------------------------------------
254
+ // Device attestation — the hardened credential path ("registration ≠ login")
255
+ // ---------------------------------------------------------------------------
256
+ // The client holds an Ed25519 PRIVATE key that never leaves this machine. Each
257
+ // session it signs a fresh server nonce (POST /api/v1/attest) and gets a 15-min
258
+ // token the governance surface already accepts — no reusable key at rest.
259
+ // Reuses the shipped enrolled-agent challenge model server-side.
260
+ const ATTEST_REFRESH_BUFFER_MS = 5 * 60_000; // refresh 5m before the 15m token expires
261
+ function generateDeviceKeypair() {
262
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
263
+ const spki = publicKey.export({ format: "der", type: "spki" });
264
+ const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" });
265
+ return { publicKeyHex: spki.subarray(-32).toString("hex"), privateKeyPkcs8B64: pkcs8.toString("base64") };
266
+ }
267
+ function signChallenge(privateKeyPkcs8B64, challenge) {
268
+ const key = createPrivateKey({ key: Buffer.from(privateKeyPkcs8B64, "base64"), format: "der", type: "pkcs8" });
269
+ return sign(null, Buffer.from(challenge, "utf8"), key).toString("base64");
270
+ }
271
+ /** A stable, valid device handle derived from the machine (host + short random). */
272
+ function defaultDeviceHandle() {
273
+ const host = hostname().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20) || "device";
274
+ const head = /^[a-z]/.test(host) ? host : `d-${host}`;
275
+ return `${head}-${randomBytes(3).toString("hex")}`.slice(0, 32);
276
+ }
277
+ /** Enroll a device public key under the org that owns `apiKey`; returns its org_id. */
278
+ async function enrollDevice(baseUrl, apiKey, handle, label, publicKeyHex) {
279
+ const resp = await apiPost(baseUrl, apiKey, "/api/v1/attest/devices", {
280
+ handle,
281
+ label,
282
+ ed25519_pubkey_hex: publicKeyHex,
283
+ });
284
+ if (resp.ok) {
285
+ const orgId = resp.body.org_id ?? "";
286
+ return orgId ? { ok: true, orgId } : { ok: false, status: resp.status, error: "enroll response missing org_id" };
287
+ }
288
+ return { ok: false, status: resp.status, error: resp.body?.error ?? `HTTP ${resp.status}` };
289
+ }
290
+ /** One attest round: challenge → sign → token. Throws with a clear message on failure. */
291
+ async function attestOnce(baseUrl, device) {
292
+ // Both attest endpoints are public (the signature is the auth), so the bearer is unused.
293
+ const ch = await apiPost(baseUrl, "", "/api/v1/attest/challenge", { org_id: device.org_id, handle: device.handle });
294
+ if (!ch.ok)
295
+ throw new Error(`challenge: ${ch.body?.error ?? `HTTP ${ch.status}`}`);
296
+ const { challenge, nonce } = ch.body;
297
+ if (!challenge || !nonce)
298
+ throw new Error("challenge response malformed");
299
+ const signature = signChallenge(device.private_key_pkcs8_b64, challenge);
300
+ const at = await apiPost(baseUrl, "", "/api/v1/attest", { org_id: device.org_id, handle: device.handle, nonce, signature });
301
+ if (!at.ok)
302
+ throw new Error(`attest: ${at.body?.error ?? `HTTP ${at.status}`}`);
303
+ const body = at.body;
304
+ if (!body.allowed || !body.access_token)
305
+ throw new Error(`attestation rejected: ${body.reason ?? "denied"}`);
306
+ return {
307
+ token: body.access_token,
308
+ expiresAtMs: body.expires_at ? Date.parse(body.expires_at) : Date.now() + 15 * 60_000,
309
+ };
310
+ }
311
+ /** A Bearer that attests lazily and refreshes before the token lapses. */
312
+ function deviceTokenProvider(baseUrl, device) {
313
+ let cached = null;
314
+ return async () => {
315
+ if (!cached || Date.now() >= cached.expiresAtMs - ATTEST_REFRESH_BUFFER_MS) {
316
+ cached = await attestOnce(baseUrl, device);
317
+ }
318
+ return cached.token;
319
+ };
320
+ }
321
+ // ---------------------------------------------------------------------------
154
322
  // Tool registration
155
323
  // ---------------------------------------------------------------------------
156
324
  function registerAdminTools(server, baseUrl, apiKey) {
@@ -372,56 +540,299 @@ function registerAgentTool(server, baseUrl, grantToken) {
372
540
  });
373
541
  return formatResult(resp);
374
542
  });
543
+ // ── Code plane · fix code without exposure, over the Fabric ─────────
544
+ // The agent drives the blind advisor on a code capsule. It sees only a
545
+ // content-blind projection (or, on an in-boundary model, a scoped slice that
546
+ // never egresses); the human sees only the changed hunks. Every turn returns
547
+ // an exposure proof + a receipt. Authorised by the capsule owner listing this
548
+ // agent's kid in scope.fabric_developers; deny-by-default.
549
+ server.registerTool("nodata_code_advise", {
550
+ description: "Ask the blind code advisor to diagnose/fix a bug in a code capsule WITHOUT seeing its source. You receive guidance + a targeted diff + an exposure proof (source never left the boundary); the source is never returned to you. Requires the capsule owner to have granted this agent (scope.fabric_developers). Hard-scoped to this server's grant token.",
551
+ inputSchema: {
552
+ capsule_id: z.string().describe("The code capsule to advise on (you must be granted on it)"),
553
+ question: z.string().optional().describe("What to fix / improve, in natural language"),
554
+ note: z.string().optional().describe("Extra context for the advisor"),
555
+ error: z
556
+ .object({
557
+ message: z.string().optional(),
558
+ stack: z.string().optional(),
559
+ kind: z.string().optional(),
560
+ })
561
+ .optional()
562
+ .describe("An error the code produced, to guide the fix"),
563
+ steer: z.string().optional().describe("A steer on a refine turn (e.g. 'no, round half-up')"),
564
+ prior_diff: z.string().optional().describe("The previously proposed diff, to refine"),
565
+ },
566
+ }, async ({ capsule_id, question, note, error, steer, prior_diff }) => {
567
+ if (!handle) {
568
+ return {
569
+ content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
570
+ isError: true,
571
+ };
572
+ }
573
+ const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/advise`, { question, note, error, steer, prior_diff });
574
+ return formatResult(resp);
575
+ });
576
+ server.registerTool("nodata_code_patch", {
577
+ description: "Submit a fix to a code capsule as a SIGNED, queued patch WITHOUT seeing its source. Provide a raw unified `diff`, or a whole-file `replace` ({path, content}) that is diffed server-side against the sealed source. The source is never persisted or returned; you get back a patch id, the diff hash, and a receipt. Requires a capsule grant (scope.fabric_developers). Hard-scoped to this server's grant token.",
578
+ inputSchema: {
579
+ capsule_id: z.string().describe("The code capsule to patch (you must be granted on it)"),
580
+ summary: z.string().optional().describe("Short human-readable summary of the fix"),
581
+ diff: z.string().optional().describe("A raw unified diff (advanced)"),
582
+ replace: z
583
+ .object({ path: z.string(), content: z.string() })
584
+ .optional()
585
+ .describe("Whole-file replacement; the diff is synthesized server-side against sealed source"),
586
+ },
587
+ }, async ({ capsule_id, summary, diff, replace }) => {
588
+ if (!handle) {
589
+ return {
590
+ content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
591
+ isError: true,
592
+ };
593
+ }
594
+ const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/patch`, { summary, diff, replace });
595
+ return formatResult(resp);
596
+ });
375
597
  }
376
- // ---------------------------------------------------------------------------
377
- // Setup mode — no credential yet
378
- // ---------------------------------------------------------------------------
379
- // The public one-liner (`claude mcp add nodata -- npx @nodatachat/mcp`) arrives
380
- // here with no credential. Exiting 1 makes the MCP client report an opaque
381
- // "connection failed" and the stderr explanation is never seen. Instead we
382
- // connect successfully and expose a single tool whose answer walks the user
383
- // (via their own AI) to a working credential.
384
- const SETUP_TEXT = "This NoData server is connected but has NO credential yet, so no data tools are available.\n\n" +
385
- "To finish setup:\n\n" +
386
- "1. Get a free API key (takes seconds, no card):\n" +
387
- " https://www.nodatacapsule.com/developers/get-started\n\n" +
388
- "2. Reconnect with the key —\n" +
389
- " Claude Code: claude mcp remove nodata\n" +
390
- " claude mcp add nodata -- npx @nodatachat/mcp --api-key sk_live_YOUR_KEY\n" +
391
- " Or keep the command credential-free and set the env var instead:\n" +
392
- " claude mcp add nodata --env NODATA_API_KEY=sk_live_YOUR_KEY -- npx @nodatachat/mcp\n\n" +
393
- " IMPORTANT: after changing the config, RESTART the MCP client (e.g. exit and reopen\n" +
394
- " Claude Code) — this server process was spawned with the old config and a config edit\n" +
395
- " does not affect it. Until the restart it will keep answering in setup mode.\n\n" +
396
- "3. (Agent mode) If you were handed a grant token (ndca-...), use --grant-token ndca-YOUR_GRANT\n" +
397
- " (or NODATA_GRANT_TOKEN) instead of an API key — the server then exposes only the\n" +
398
- " decide/retrieve/read/use tools scoped to that grant.\n\n" +
399
- "Admin mode (--api-key) manages governance: register tables, issue/revoke grants, encrypt,\n" +
400
- "decrypt, deliver, evidence. Agent mode (--grant-token) is what you hand an AI agent.";
401
- function registerSetupTool(server) {
598
+ const START_ACHIEVEMENT = "WHAT NODATA DOES — in one line:\n" +
599
+ " Give any agent real power over your data, without the power to leak it.\n" +
600
+ " (Possession is not permission: it can hold the key and still not read what you didn't allow.)\n";
601
+ // Setup mode: NOT connected yet. The one job here is to switch the tools on, so
602
+ // this is a numbered do-this-exactly list — three steps, not a catalogue. The
603
+ // last line answers the two questions everyone asks: "how do I activate?" (/mcp)
604
+ // and "what about next time?" (nothing).
605
+ const SETUP_STEPS = "TURN THE DATA TOOLS ON — 3 steps, about a minute, once:\n\n" +
606
+ " 1. GET A KEY call nodata_connect (no arguments)\n" +
607
+ " -> opens the signup in your browser: email + company (~30s), a 12-word\n" +
608
+ " recovery phrase to keep, and an ORG KEY shown ONCE. Copy that key.\n\n" +
609
+ " 2. SAVE IT call nodata_connect again, this time with\n" +
610
+ " api_key: \"<the key you copied>\"\n" +
611
+ " -> saved on this machine; you never paste it again.\n\n" +
612
+ " 3. ACTIVATE type /mcp (or restart Claude Code) so the tools load.\n\n" +
613
+ "COMING BACK LATER? Nothing to redo — it starts ready. If the tools look missing, just /mcp.\n";
614
+ // Admin mode: connected. Frame the whole product as the developer's real journey
615
+ // — SEE -> CHOOSE -> PROVE — each step a plain-language ask mapped to a tool.
616
+ const START_OUTCOMES_ADMIN = "NOW — TURN YOUR DATA INTO AN AGENT THAT CAN'T LEAK, in 3 asks (just say them in plain words):\n\n" +
617
+ " 1. SEE what's sensitive open https://www.nodatacapsule.com/ai-folder\n" +
618
+ " -> pick a folder; it shows what an AI could read. Nothing uploads.\n\n" +
619
+ " 2. CHOOSE what to release say \"let <agent> read name + plan on <table>, hide the rest\"\n" +
620
+ " -> I run nodata_register_table + nodata_grant; the agent gets a token.\n\n" +
621
+ " 3. PROVE & control the agent now reads ONLY those columns — the rest never decrypt.\n" +
622
+ " \"what did it see?\" -> nodata_evidence · \"cut it off\" -> nodata_revoke\n\n" +
623
+ "Also: a secret even NoData can't read -> nodata_encrypt, then hand it over once -> nodata_deliver.\n";
624
+ // The agent-scoped reader IS the governed agent: it holds only decide/retrieve/read/use,
625
+ // so its steps name those verbs (pointing it at admin tools it lacks would dead-end it).
626
+ const START_OUTCOMES_AGENT = "WHAT YOU CAN DO — scoped to your grant; you reach only what it allows, the rest never decrypts:\n\n" +
627
+ " 1. PLAN nodata_decide \"may I reach X?\" -> allow / partial / deny + a signed proof. No data moves.\n" +
628
+ " 2. PULL nodata_retrieve prompt-ready context, allowed fields only, with a receipt.\n" +
629
+ " (nodata_read for raw governed rows.)\n" +
630
+ " 3. ACT nodata_use run a capability with a secret injected server-side — you never see it.\n";
631
+ const START_UNSTUCK = "STUCK? There's always a next step:\n" +
632
+ " - Don't see the nodata_* tools? They load when the client starts — type /mcp or restart.\n" +
633
+ " (Expected, not an error.)\n" +
634
+ " - No MCP at all? The same governance runs from one curl, or the guided page:\n" +
635
+ " https://www.nodatacapsule.com/developers/get-started\n";
636
+ function startHeader(mode) {
637
+ if (mode === "setup") {
638
+ return "You're connected to NoData — but there's NO key yet, so the data tools are still off.\n\n";
639
+ }
640
+ if (mode === "agent") {
641
+ return "You're connected to NoData, scoped to one grant token.\n\n";
642
+ }
643
+ return "You're connected to NoData (admin mode). You don't memorize commands — just ask in plain words.\n\n";
644
+ }
645
+ function getStartedText(mode) {
646
+ // Setup leads with the 3 connect steps (the only thing that matters when off);
647
+ // admin/agent lead with the journey they can act on right now.
648
+ if (mode === "setup") {
649
+ return startHeader(mode) + START_ACHIEVEMENT + "\n" + SETUP_STEPS + "\n" + START_UNSTUCK;
650
+ }
651
+ const outcomes = mode === "agent" ? START_OUTCOMES_AGENT : START_OUTCOMES_ADMIN;
652
+ return startHeader(mode) + START_ACHIEVEMENT + "\n" + outcomes + "\n" + START_UNSTUCK;
653
+ }
654
+ function registerGetStarted(server, mode) {
402
655
  server.registerTool("nodata_get_started", {
403
- description: "NoData is connected WITHOUT a credential — data tools are disabled. Call this (no arguments) to get the exact steps to finish setup: where to get a free API key and how to reconnect in admin or agent mode. If the user asks why NoData tools are missing or how to set up NoData, call this and relay the steps.",
656
+ description: "Call this FIRST (no arguments). NoData in one line: what you can achieve — an agent that " +
657
+ "cannot leak, a look at what is already exposed, a secret we cannot read — with the single " +
658
+ "next step for each, and how to recover if the tools or MCP seem stuck. If the user asks what " +
659
+ "NoData can do, how to start, or why the tools are missing, call this and relay it.",
404
660
  inputSchema: {},
405
- }, async () => ({ content: [{ type: "text", text: SETUP_TEXT }] }));
661
+ }, async () => ({ content: [{ type: "text", text: getStartedText(mode) }] }));
662
+ }
663
+ // ---------------------------------------------------------------------------
664
+ // nodata_connect — setup-mode "get switched on without re-typing the command"
665
+ // ---------------------------------------------------------------------------
666
+ // "Registration ≠ login." Call once with no args → open the real signup in the
667
+ // browser (email + company, the 12-word recovery, the email verify — the parts
668
+ // a headless mint would skip). It shows the org key once; call again with that
669
+ // key → it is saved to ~/.nodata and every later start autoloads it. First run:
670
+ // register + one paste. Every run after: nothing. The only residual is one /mcp,
671
+ // because MCP clients read their tool list at startup (a client fact, not ours).
672
+ const REGISTER_URL_PATH = "/capsule-api/register";
673
+ function openInBrowser(url) {
674
+ const isWin = process.platform === "win32";
675
+ const cmd = process.platform === "darwin" ? "open" : isWin ? "cmd" : "xdg-open";
676
+ const args = isWin ? ["/c", "start", "", url] : [url];
677
+ try {
678
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
679
+ // A missing opener (headless box, no xdg-open) must never surface as a tool
680
+ // error — the URL is always printed too, so the flow proceeds regardless.
681
+ child.on("error", () => { });
682
+ child.unref();
683
+ return true;
684
+ }
685
+ catch {
686
+ return false;
687
+ }
688
+ }
689
+ function registerConnectTool(server, baseUrl) {
690
+ server.registerTool("nodata_connect", {
691
+ description: "Switch NoData's data tools on. Step 1: call with NO arguments to open the free signup in a " +
692
+ "browser (email + company, ~30s — also where your 12-word recovery phrase + email verification " +
693
+ "happen); it shows an ORG KEY once. Step 2: call this tool again with that key as `api_key` and " +
694
+ "it is saved on this machine, so every later start comes up ready with nothing to retype. " +
695
+ "Step 3: type /mcp (or restart) so the client loads the tools.",
696
+ inputSchema: {
697
+ api_key: z
698
+ .string()
699
+ .optional()
700
+ .describe("The org key shown once at signup (ndp_… or sk_…). Omit on the first call to open signup."),
701
+ },
702
+ }, async ({ api_key }) => {
703
+ const key = (api_key ?? "").trim();
704
+ // First call (no key): open the real signup and tell them to come back with it.
705
+ if (!key) {
706
+ const url = baseUrl + REGISTER_URL_PATH;
707
+ const opened = openInBrowser(url);
708
+ return {
709
+ content: [
710
+ {
711
+ type: "text",
712
+ text: (opened ? "STEP 1 of 3 — opened the NoData signup in your browser:\n " : "STEP 1 of 3 — open the NoData signup in your browser:\n ") +
713
+ url +
714
+ "\n\n" +
715
+ "There, about a minute:\n" +
716
+ " - email + company (a personal email is fine for the sandbox)\n" +
717
+ " - a 12-WORD RECOVERY PHRASE — save it; it is the only way back into your org\n" +
718
+ " - your ORG KEY, shown ONCE — copy it\n\n" +
719
+ "STEP 2 — come back and call this tool again with the key:\n" +
720
+ ' nodata_connect api_key: "<the key you copied>"\n' +
721
+ "and I'll save it so you never paste it again. (Then STEP 3: type /mcp to activate.)",
722
+ },
723
+ ],
724
+ };
725
+ }
726
+ // Shape check — don't persist something that clearly isn't a NoData key.
727
+ if (!/^(ndp_|sk_)/.test(key)) {
728
+ return {
729
+ content: [
730
+ {
731
+ type: "text",
732
+ text: "That doesn't look like a NoData org key (it should start with ndp_ or sk_).\n" +
733
+ 'Copy the key shown once on the signup page and try again: nodata_connect api_key: "ndp_…".',
734
+ },
735
+ ],
736
+ };
737
+ }
738
+ // Preferred path: enroll a DEVICE KEY so no reusable key is stored. Generate
739
+ // a keypair, register its PUBLIC half under the org (authenticated by the key
740
+ // the user just pasted), keep only the PRIVATE half. Then every start attests.
741
+ const { publicKeyHex, privateKeyPkcs8B64 } = generateDeviceKeypair();
742
+ const handle = defaultDeviceHandle();
743
+ const label = hostname() || "this device";
744
+ const enrolled = await enrollDevice(baseUrl, key, handle, label, publicKeyHex);
745
+ if (enrolled.ok) {
746
+ try {
747
+ writeStoredCredential({
748
+ device: {
749
+ org_id: enrolled.orgId,
750
+ handle,
751
+ label,
752
+ private_key_pkcs8_b64: privateKeyPkcs8B64,
753
+ public_key_hex: publicKeyHex,
754
+ },
755
+ base_url: baseUrl,
756
+ });
757
+ }
758
+ catch (e) {
759
+ return {
760
+ content: [{ type: "text", text: "Enrolled the device, but couldn't save it locally: " + (e instanceof Error ? e.message : String(e)) }],
761
+ };
762
+ }
763
+ return {
764
+ content: [
765
+ {
766
+ type: "text",
767
+ text: `STEP 2 done — this device is enrolled as "${label}" (${handle}), and only its PRIVATE key is\n` +
768
+ "saved at ~/.nodata/credentials.json (readable only by you). The org key you pasted was NOT\n" +
769
+ "stored: every start now proves possession by signing a fresh challenge and gets a 15-minute token.\n\n" +
770
+ "STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
771
+ },
772
+ ],
773
+ };
774
+ }
775
+ // Fallback: a backend that predates the attest endpoints. Store the key at
776
+ // rest (the Part-2 path) so onboarding still works end to end.
777
+ const attestUnavailable = enrolled.status === 404 || enrolled.status === 0;
778
+ try {
779
+ writeStoredCredential({ api_key: key, key_prefix: key.slice(0, 12), base_url: baseUrl });
780
+ }
781
+ catch (e) {
782
+ return {
783
+ content: [
784
+ {
785
+ type: "text",
786
+ text: "Couldn't save locally: " +
787
+ (e instanceof Error ? e.message : String(e)) +
788
+ "\nAs a fallback, pass it on launch: claude mcp add nodata --env NODATA_API_KEY=<your key> -- npx @nodatachat/mcp",
789
+ },
790
+ ],
791
+ };
792
+ }
793
+ return {
794
+ content: [
795
+ {
796
+ type: "text",
797
+ text: (attestUnavailable
798
+ ? "STEP 2 done — saved your key on this machine (device attestation isn't available on this\nserver yet, so this is the key-at-rest path). "
799
+ : `STEP 2 done — saved your key on this machine (device enrollment didn't succeed: ${enrolled.error}). `) +
800
+ "Every future start loads it automatically — no re-register, no re-paste.\n\n" +
801
+ "STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
802
+ },
803
+ ],
804
+ };
805
+ });
406
806
  }
407
807
  // ---------------------------------------------------------------------------
408
808
  // Server setup
409
809
  // ---------------------------------------------------------------------------
410
810
  async function main() {
411
- const { apiKey, grantToken, baseUrl } = parseArgs(process.argv.slice(2));
412
- const mode = grantToken ? "agent" : apiKey ? "admin" : "setup";
811
+ const { apiKey, grantToken, baseUrl, credentialSource, device } = parseArgs(process.argv.slice(2));
812
+ const mode = grantToken ? "agent" : apiKey || device ? "admin" : "setup";
413
813
  const server = new McpServer({
414
814
  name: mode === "agent" ? "NoData (agent-scoped)" : mode === "setup" ? "NoData (setup)" : "NoData",
415
815
  version: VERSION,
416
816
  });
417
- if (apiKey)
817
+ // A stored device (attest) is preferred: the admin tools get a token PROVIDER
818
+ // that signs a fresh challenge per session instead of a static key.
819
+ if (device)
820
+ registerAdminTools(server, baseUrl, deviceTokenProvider(baseUrl, device));
821
+ else if (apiKey)
418
822
  registerAdminTools(server, baseUrl, apiKey);
419
823
  if (grantToken)
420
824
  registerAgentTool(server, baseUrl, grantToken);
825
+ registerGetStarted(server, mode);
421
826
  if (mode === "setup") {
422
- registerSetupTool(server);
423
- console.error("nodata-mcp: no credential given — running in setup mode (only nodata_get_started is available).\n" +
424
- "Pass --api-key sk_live_... or --grant-token ndca-... to enable data tools. See --help.");
827
+ registerConnectTool(server, baseUrl);
828
+ console.error("nodata-mcp: no credential given — running in setup mode (data tools disabled).\n" +
829
+ "Call nodata_connect (opens signup, enrolls this device), or pass --api-key / --grant-token. See --help.");
830
+ }
831
+ else if (device) {
832
+ console.error(`nodata-mcp: loaded device identity "${device.label}" (${device.handle}) — attest mode; tokens are minted per session.`);
833
+ }
834
+ else if (credentialSource === "stored") {
835
+ console.error("nodata-mcp: loaded saved credential from ~/.nodata/credentials.json (admin mode).");
425
836
  }
426
837
  const transport = new StdioServerTransport();
427
838
  await server.connect(transport);
package/package.json CHANGED
@@ -1,38 +1,43 @@
1
- {
2
- "name": "@nodatachat/mcp",
3
- "version": "0.4.2",
4
- "description": "NoData MCP Server — govern data for AI agents: decide (may I?) + retrieve (authorized context) + read + secret-blind invoke, plus admin governance/encrypt/decrypt/deliver",
5
- "bin": {
6
- "nodata-mcp": "dist/index.js"
7
- },
8
- "type": "module",
9
- "scripts": {
10
- "build": "tsc",
11
- "dev": "tsx src/index.ts"
12
- },
13
- "dependencies": {
14
- "@modelcontextprotocol/sdk": "^1.29.0",
15
- "zod": "^4.0.0"
16
- },
17
- "devDependencies": {
18
- "@types/node": "^22.0.0",
19
- "tsx": "^4.0.0",
20
- "typescript": "^5.7.0"
21
- },
22
- "engines": {
23
- "node": ">=18.0.0"
24
- },
25
- "files": [
26
- "dist"
27
- ],
28
- "homepage": "https://www.nodatacapsule.com/developers",
29
- "keywords": [
30
- "mcp",
31
- "model-context-protocol",
32
- "ai-agents",
33
- "data-governance",
34
- "nodata",
35
- "access-control"
36
- ],
37
- "license": "MIT"
38
- }
1
+ {
2
+ "name": "@nodatachat/mcp",
3
+ "version": "0.8.0",
4
+ "description": "NoData MCP Server — govern data AND code for AI agents: decide (may I?) + retrieve (authorized context) + read + secret-blind invoke + blind code advise/patch (fix code without exposure), plus admin governance/encrypt/decrypt/deliver",
5
+ "bin": {
6
+ "nodata-mcp": "dist/index.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.29.0",
16
+ "zod": "^4.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "tsx": "^4.0.0",
21
+ "typescript": "^5.7.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "homepage": "https://www.nodatacapsule.com/developers",
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "ai-agents",
37
+ "data-governance",
38
+ "code-capsule",
39
+ "nodata",
40
+ "access-control"
41
+ ],
42
+ "license": "MIT"
43
+ }