datum-mcp-server 1.5.0 → 1.6.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.
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+ // Locate the MCP client config file an operator's agent reads, and merge
3
+ // Datum's server entry into it without disturbing anything else.
4
+ //
5
+ // Why write the config file directly instead of shelling out to
6
+ // `openclaw mcp add` / `claude mcp add`: those take the seller key as a
7
+ // command-line argument, where it is visible to `ps` for the life of the call
8
+ // and lands in shell history. A direct write keeps the key out of argv, out of
9
+ // history, and out of any log.
10
+ //
11
+ // Refusals over guesses: a config that does not parse, or whose target key is
12
+ // not an object, is reported as an error and never rewritten.
13
+
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ const SERVER_NAME = 'datum';
19
+ const DEFAULT_RPC = 'https://mainnet.base.org';
20
+ const SERVER_COMMAND = 'npx';
21
+ const SERVER_ARGS = ['-y', 'datum-mcp-server'];
22
+
23
+ function candidates({ cwd = process.cwd(), home = os.homedir(), platform = process.platform, env = process.env } = {}) {
24
+ const list = [
25
+ { id: 'openclaw', label: 'OpenClaw', file: path.join(home, '.openclaw', 'openclaw.json'), root: ['mcp', 'servers'] },
26
+ { id: 'claude-code', label: 'Claude Code (project)', file: path.join(cwd, '.mcp.json'), root: ['mcpServers'] },
27
+ { id: 'claude-code-user', label: 'Claude Code (user)', file: path.join(home, '.claude.json'), root: ['mcpServers'] },
28
+ ];
29
+ if (platform === 'darwin') {
30
+ list.push({
31
+ id: 'claude-desktop',
32
+ label: 'Claude Desktop',
33
+ file: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
34
+ root: ['mcpServers'],
35
+ });
36
+ } else if (platform === 'win32') {
37
+ list.push({
38
+ id: 'claude-desktop',
39
+ label: 'Claude Desktop',
40
+ file: path.join(env.APPDATA || home, 'Claude', 'claude_desktop_config.json'),
41
+ root: ['mcpServers'],
42
+ });
43
+ } else {
44
+ list.push({
45
+ id: 'claude-desktop',
46
+ label: 'Claude Desktop',
47
+ file: path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
48
+ root: ['mcpServers'],
49
+ });
50
+ }
51
+ return list;
52
+ }
53
+
54
+ function inspect(file) {
55
+ const out = { exists: false, isFile: false, bytes: 0, writable: false, valid: true, error: null };
56
+ try {
57
+ const st = fs.statSync(file);
58
+ out.exists = true;
59
+ out.isFile = st.isFile();
60
+ out.bytes = st.size;
61
+ } catch {
62
+ out.exists = false;
63
+ }
64
+ try {
65
+ if (out.exists) fs.accessSync(file, fs.constants.R_OK | fs.constants.W_OK);
66
+ else fs.accessSync(path.dirname(file), fs.constants.W_OK);
67
+ out.writable = true;
68
+ } catch {
69
+ out.writable = false;
70
+ }
71
+ if (out.exists && out.isFile) {
72
+ try {
73
+ JSON.parse(fs.readFileSync(file, 'utf8'));
74
+ } catch {
75
+ out.valid = false;
76
+ out.error = 'existing config is not valid JSON — refusing to rewrite it';
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+
82
+ function detect(opts) {
83
+ return candidates(opts).map((c) => Object.assign({}, c, inspect(c.file)));
84
+ }
85
+
86
+ /**
87
+ * Choose a config to write. Only an existing, writable, parseable config is
88
+ * ever selected — this tool never creates a client config from nothing, because
89
+ * guessing a client's location is how you end up writing to the wrong file.
90
+ */
91
+ function pick(cands, explicit) {
92
+ if (explicit) {
93
+ const file = path.resolve(explicit);
94
+ const known = cands.find((c) => path.resolve(c.file) === file);
95
+ const base = { id: 'explicit', label: 'explicit --client-config', file, root: ['mcpServers'] };
96
+ return Object.assign(base, known ? { root: known.root } : {}, inspect(file));
97
+ }
98
+ return cands.find((c) => c.exists && c.isFile && c.writable && c.valid) || null;
99
+ }
100
+
101
+ /** The server entry written into the client config. */
102
+ function serverEntry({ key, rpc = DEFAULT_RPC, cap } = {}) {
103
+ const env = { DATUM_RPC_URL: rpc };
104
+ if (cap !== undefined && cap !== null && cap !== '') env.DATUM_MAX_PRICE_DTM = String(cap);
105
+ if (key) env.DATUM_SIGNER_KEY = key;
106
+ return { command: SERVER_COMMAND, args: SERVER_ARGS.slice(), env };
107
+ }
108
+
109
+ /**
110
+ * Merge `entry` under `root`/`name`, mutating `parsed`.
111
+ * @returns {{action:'added'|'updated'|'unchanged'|'error', error?:string}}
112
+ */
113
+ function mergeServer(parsed, root, name, entry) {
114
+ let node = parsed;
115
+ for (const k of root) {
116
+ if (node[k] === undefined) node[k] = {};
117
+ if (typeof node[k] !== 'object' || node[k] === null || Array.isArray(node[k])) {
118
+ return { action: 'error', error: `config key "${k}" is not an object — refusing to overwrite it` };
119
+ }
120
+ node = node[k];
121
+ }
122
+ if (node[name] && JSON.stringify(node[name]) === JSON.stringify(entry)) return { action: 'unchanged' };
123
+ const action = node[name] ? 'updated' : 'added';
124
+ node[name] = entry;
125
+ return { action };
126
+ }
127
+
128
+ /**
129
+ * Compute the new file content without touching the filesystem.
130
+ * @returns {{action:string, content?:string, error?:string}}
131
+ */
132
+ function planConfig(file, root, name, entry) {
133
+ let parsed = {};
134
+ if (fs.existsSync(file)) {
135
+ let raw;
136
+ try {
137
+ raw = fs.readFileSync(file, 'utf8');
138
+ } catch (e) {
139
+ return { action: 'error', error: `cannot read config: ${e.message}` };
140
+ }
141
+ if (raw.trim() !== '') {
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ } catch {
145
+ return { action: 'error', error: 'existing config is not valid JSON — refusing to rewrite it' };
146
+ }
147
+ }
148
+ }
149
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
150
+ return { action: 'error', error: 'config root is not a JSON object — refusing to rewrite it' };
151
+ }
152
+ const merged = mergeServer(parsed, root, name, entry);
153
+ if (merged.action === 'error') return merged;
154
+ return { action: merged.action, content: JSON.stringify(parsed, null, 2) + '\n' };
155
+ }
156
+
157
+ /** The seller key already present in a config, so a re-run never drops it. */
158
+ function existingKey(file, root, name) {
159
+ try {
160
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
161
+ let node = parsed;
162
+ for (const k of root) {
163
+ node = node ? node[k] : null;
164
+ if (!node) return null;
165
+ }
166
+ const entry = node[name];
167
+ return (entry && entry.env && entry.env.DATUM_SIGNER_KEY) || null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /** Ready-to-paste block for clients we cannot write directly (key left blank). */
174
+ function pasteBlock({ cap } = {}) {
175
+ const env = { DATUM_SIGNER_KEY: 'REPLACE_WITH_YOUR_KEY', DATUM_RPC_URL: DEFAULT_RPC };
176
+ if (cap !== undefined && cap !== null && cap !== '') env.DATUM_MAX_PRICE_DTM = String(cap);
177
+ const body = { mcpServers: {} };
178
+ body.mcpServers[SERVER_NAME] = { command: SERVER_COMMAND, args: SERVER_ARGS.slice(), env };
179
+ return JSON.stringify(body, null, 2);
180
+ }
181
+
182
+ module.exports = {
183
+ SERVER_NAME,
184
+ DEFAULT_RPC,
185
+ candidates,
186
+ inspect,
187
+ detect,
188
+ pick,
189
+ serverEntry,
190
+ mergeServer,
191
+ planConfig,
192
+ existingKey,
193
+ pasteBlock,
194
+ };
package/onboard.js CHANGED
@@ -1,34 +1,44 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  /*
4
- * datum-onboard — put the Datum every-task policy where your agent reads it.
4
+ * datum-onboard — set an agent up on Datum in one command.
5
5
  *
6
- * The policy block is what makes an agent search the market before it collects
7
- * data, and list what it produces (including methods and negative results)
8
- * after every task. Agents do not reliably discover this on their own: a
9
- * standing instruction file is the delivery channel that works.
6
+ * Three guided steps, each one confirmation:
7
+ * 1. seller wallet — generated locally; the key goes straight into the MCP
8
+ * client config (0600) and is NEVER printed
9
+ * 2. connection — the Datum MCP server registered in that client config,
10
+ * which is what makes the 14 datum tools exist
11
+ * 3. policy — the every-task block in the agent's instruction file
10
12
  *
11
- * This tool is intentionally boring and safe:
12
- * * it writes exactly one file, chosen by you, with a marker-wrapped block
13
- * * it never deletes content, never prints a key, and never touches the
14
- * network
15
- * * re-running it is a no-op (the block is replaced in place, idempotently)
16
- * * it refuses to write policy into credential-looking files
13
+ * Safety properties, all deliberate:
14
+ * * the key never appears in argv, in shell history, in stdout, or in --json
15
+ * (that is why this writes the client config directly instead of shelling
16
+ * out to `openclaw mcp add` / `claude mcp add`, which take it as an argument)
17
+ * * every file is backed up to <file>.datum-bak before its first change and
18
+ * then written atomically (temp + rename), mode 0600
19
+ * * a config that does not parse, or a target key that is not an object, is a
20
+ * hard error — this never guesses and never rewrites what it cannot read
21
+ * * re-running is a no-op: the server entry is replaced in place, the policy
22
+ * is marker-wrapped, and an existing seller key is reused rather than
23
+ * regenerated
24
+ * * zero network calls
17
25
  *
18
26
  * Usage:
19
- * npx datum-onboard # detect, confirm, write
20
- * npx datum-onboard --list # show candidate instruction files
21
- * npx datum-onboard --print # print the block, change nothing
22
- * npx datum-onboard --dry-run # show what would change
23
- * npx datum-onboard --yes # non-interactive (for automation)
24
- * npx datum-onboard --target ./CLAUDE.md
27
+ * npx datum-onboard # detect, then confirm each step
28
+ * npx datum-onboard --yes # non-interactive: accept all steps
29
+ * npx datum-onboard --list # show candidate files for this machine
30
+ * npx datum-onboard --print # print the policy block, change nothing
31
+ * npx datum-onboard --dry-run # show every planned change
32
+ * npx datum-onboard --target <file> # explicit instruction file
33
+ * npx datum-onboard --client-config <file> # explicit MCP client config
34
+ * npx datum-onboard --no-connect # policy only (skip server + wallet)
25
35
  */
26
36
 
27
37
  const fs = require('fs');
28
38
  const path = require('path');
29
- const { spawnSync } = require('child_process');
30
39
  const { detect } = require('./onboard/detect');
31
- const { mergePolicy, hasPolicy } = require('./onboard/merge');
40
+ const { mergePolicy } = require('./onboard/merge');
41
+ const mcpClient = require('./onboard/client');
32
42
  const pkg = require('./package.json');
33
43
 
34
44
  const BLOCK_PATH = path.join(__dirname, 'onboard', 'policy-block.md');
@@ -36,7 +46,10 @@ const DEFAULT_CAP = 1000;
36
46
  const KEY_FILE_BASENAMES = ['.env', '.env.local', 'id_rsa', 'id_ed25519', 'id_ecdsa'];
37
47
 
38
48
  function parseArgs(argv) {
39
- const a = { yes: false, dryRun: false, list: false, print: false, json: false, help: false, target: null, cap: null };
49
+ const a = {
50
+ yes: false, dryRun: false, list: false, print: false, json: false, help: false,
51
+ target: null, cap: null, clientConfig: null, noConnect: false, noWallet: false,
52
+ };
40
53
  for (let i = 0; i < argv.length; i++) {
41
54
  const v = argv[i];
42
55
  if (v === '--yes' || v === '-y') a.yes = true;
@@ -47,6 +60,9 @@ function parseArgs(argv) {
47
60
  else if (v === '--help' || v === '-h') a.help = true;
48
61
  else if (v === '--target') a.target = argv[++i];
49
62
  else if (v === '--cap') a.cap = argv[++i];
63
+ else if (v === '--client-config') a.clientConfig = argv[++i];
64
+ else if (v === '--no-connect') { a.noConnect = true; a.noWallet = true; }
65
+ else if (v === '--no-wallet') a.noWallet = true;
50
66
  else { console.error(`datum-onboard: unknown option "${v}" (try --help)`); process.exit(2); }
51
67
  }
52
68
  return a;
@@ -61,6 +77,10 @@ function loadBlock(cap) {
61
77
  return raw.replace(/\{\{MAX_PRICE_DTM\}\}/g, resolved);
62
78
  }
63
79
 
80
+ function capFromBlock(block, fallback) {
81
+ return /Hard cap: (\d+) DTM/.exec(block)?.[1] || String(fallback || DEFAULT_CAP);
82
+ }
83
+
64
84
  /** Refuse targets that look like credential stores. */
65
85
  function assertSafeTarget(file) {
66
86
  const base = path.basename(file).toLowerCase();
@@ -96,24 +116,50 @@ function chooseTarget(flags, found) {
96
116
  return null;
97
117
  }
98
118
 
119
+ /** Generate a wallet lazily so `ethers` is only loaded when actually needed. */
120
+ function generateWallet() {
121
+ const { Wallet } = require('ethers');
122
+ return Wallet.createRandom();
123
+ }
124
+
125
+ function addressForKey(key) {
126
+ try {
127
+ const { Wallet } = require('ethers');
128
+ return new Wallet(key).address;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
99
134
  function help() {
100
- console.log(`datum-onboard ${pkg.version} — install the Datum every-task policy for your agent.
135
+ console.log(`datum-onboard ${pkg.version} — set an agent up on Datum, guided.
101
136
 
102
137
  Usage: npx datum-onboard [options]
103
138
 
104
- --list show candidate instruction files for this machine
139
+ --list show candidate instruction files and MCP client configs
105
140
  --print print the policy block and exit (writes nothing)
106
141
  --dry-run show what would change, write nothing
107
- --target <file> write to this file instead of auto-detecting
108
- --cap <n> spend cap written into the policy (default ${DEFAULT_CAP} DTM,
109
- or $DATUM_MAX_PRICE_DTM)
110
- --yes, -y accept defaults, no prompts (for automation)
111
- --json machine-readable summary on stdout
142
+ --target <file> instruction file to write the policy into
143
+ (default: auto-detected — OpenClaw AGENTS.md, CLAUDE.md, ...)
144
+ --client-config <file> MCP client config to register the server in
145
+ --cap <n> spend cap written into the policy and the client env
146
+ (default ${DEFAULT_CAP} DTM, or $DATUM_MAX_PRICE_DTM)
147
+ --no-connect policy only: do not register the server or create a wallet
148
+ --no-wallet keep any existing key; do not generate a new one
149
+ --yes, -y accept every step, no prompts (for automation)
150
+ --json machine-readable summary on stdout (never contains the key)
112
151
  -h, --help this text
113
152
 
114
- The written block is marker-wrapped and replaced in place on re-runs, so your
115
- other instructions are never disturbed. Existing files are backed up to
116
- <file>.datum-bak before the first change.`);
153
+ Three steps, each one confirmation:
154
+ 1. seller wallet the private key is written to your MCP client config (0600)
155
+ and is never printed to this terminal
156
+ 2. connection the Datum MCP server is registered in that config, which is
157
+ what gives your agent the datum tools
158
+ 3. policy the every-task block is written into your agent's
159
+ instruction file, marker-wrapped and replaced in place
160
+
161
+ Every file is backed up to <file>.datum-bak before its first change. Re-running
162
+ is a no-op. Nothing is sent anywhere by this tool.`);
117
163
  }
118
164
 
119
165
  async function main() {
@@ -123,100 +169,152 @@ async function main() {
123
169
  const block = loadBlock(flags.cap);
124
170
  if (flags.print) { process.stdout.write(block); return; }
125
171
 
126
- const found = detect();
172
+ const instructionFiles = detect();
173
+ const clientConfigs = mcpClient.detect();
174
+ const cap = capFromBlock(block, flags.cap || process.env.DATUM_MAX_PRICE_DTM);
127
175
 
128
176
  if (flags.list) {
129
- for (const c of found) {
177
+ console.log('Instruction files (where the policy goes):');
178
+ for (const c of instructionFiles) {
130
179
  const state = c.exists ? (c.isFile ? `${c.size} bytes` : 'not a regular file') : 'missing';
131
- console.log(`${c.id.padEnd(12)} ${c.writable ? 'writable' : 'read-only'} ${state.padEnd(18)} ${c.file}`);
180
+ console.log(` ${c.id.padEnd(14)} ${c.writable ? 'writable' : 'read-only'} ${state.padEnd(18)} ${c.file}`);
181
+ }
182
+ console.log('\nMCP client configs (where the server is registered):');
183
+ for (const c of clientConfigs) {
184
+ const state = c.exists ? (c.valid ? `${c.bytes} bytes` : `present, ${c.error}`) : 'missing';
185
+ console.log(` ${c.id.padEnd(14)} ${c.writable ? 'writable' : 'read-only'} ${state.padEnd(18)} ${c.file}`);
132
186
  }
133
187
  return;
134
188
  }
135
189
 
136
- const target = chooseTarget(flags, found);
190
+ const target = chooseTarget(flags, instructionFiles);
137
191
  if (!target) {
138
192
  console.error('datum-onboard: no writable instruction file found. Use --target <file>.');
139
193
  process.exit(1);
140
194
  }
141
195
 
142
- const existing = fs.existsSync(target.file) ? fs.readFileSync(target.file, 'utf8') : '';
143
- const merged = mergePolicy(existing, block);
144
-
145
- if (merged.action === 'error') {
146
- console.error(`datum-onboard: ${merged.error} (${target.file})`);
196
+ const existingText = fs.existsSync(target.file) ? fs.readFileSync(target.file, 'utf8') : '';
197
+ const policyPlan = mergePolicy(existingText, block);
198
+ if (policyPlan.action === 'error') {
199
+ console.error(`datum-onboard: ${policyPlan.error} (${target.file})`);
147
200
  process.exit(1);
148
201
  }
149
202
 
150
- const summary = {
151
- tool: 'datum-onboard',
152
- version: pkg.version,
153
- target: target.file,
154
- detected: target.why,
155
- action: merged.action,
156
- cap: /Hard cap: (\d+) DTM/.exec(block)?.[1] || String(DEFAULT_CAP),
157
- wrote: false,
158
- keyPresent: Boolean(process.env.DATUM_SIGNER_KEY),
159
- };
160
-
161
- if (merged.action === 'unchanged') {
162
- if (flags.json) console.log(JSON.stringify({ ...summary, alreadyCurrent: true }, null, 2));
163
- else console.log(`Already current — ${target.file} carries the current Datum policy. Nothing to do.`);
164
- return;
165
- }
166
-
167
- if (flags.dryRun) {
168
- if (flags.json) console.log(JSON.stringify({ ...summary, dryRun: true }, null, 2));
169
- else console.log(`Would ${merged.action === 'added' ? 'add' : 'update'} the Datum policy in:\n ${target.file}\n(dry run — nothing written)`);
170
- return;
171
- }
203
+ // The client is chosen first: a generated key must have somewhere to live, so
204
+ // we never mint one we cannot store.
205
+ const chosen = flags.noConnect ? null : mcpClient.pick(clientConfigs, flags.clientConfig);
172
206
 
173
207
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
174
-
175
208
  if (!flags.yes && !interactive) {
176
209
  console.error('datum-onboard: not a TTY and --yes was not given. Re-run with --yes to accept defaults.');
177
210
  process.exit(2);
178
211
  }
212
+ const rl = (interactive && !flags.yes)
213
+ ? require('readline/promises').createInterface({ input: process.stdin, output: process.stdout })
214
+ : null;
215
+ const ask = async (q) => (rl ? /^y(es)?$/i.test(String(await rl.question(q)).trim()) : true);
216
+ const say = (...a) => { if (!flags.json) console.log(...a); };
217
+
218
+ const summary = {
219
+ tool: 'datum-onboard',
220
+ version: pkg.version,
221
+ cap,
222
+ dryRun: Boolean(flags.dryRun),
223
+ wallet: { reused: false, generated: false, address: null },
224
+ client: chosen
225
+ ? { id: chosen.id, label: chosen.label, file: chosen.file, action: 'skipped', wrote: false }
226
+ : null,
227
+ policy: { file: target.file, detected: target.why, action: policyPlan.action, wrote: false },
228
+ };
179
229
 
180
- let confirm = flags.yes;
181
- if (!confirm) {
182
- const rl = require('readline/promises').createInterface({ input: process.stdin, output: process.stdout });
183
- const verb = merged.action === 'added' ? 'Add' : 'Update';
184
- const answer = await rl.question(`${verb} the Datum every-task policy in:\n ${target.file}\nProceed? [y/N] `);
185
- confirm = /^y(es)?$/i.test(answer.trim());
186
- if (!confirm) { rl.close(); console.log('Aborted — nothing written.'); return; }
230
+ // ---------------------------------------------------------------- step 1/3
231
+ let key = (process.env.DATUM_SIGNER_KEY || '').trim() || null;
232
+ if (!key && chosen) key = mcpClient.existingKey(chosen.file, chosen.root, mcpClient.SERVER_NAME);
187
233
 
188
- if (summary.keyPresent) {
189
- console.log('\nSeller key detected (DATUM_SIGNER_KEY) — buying and selling are enabled.');
234
+ if (key) {
235
+ summary.wallet.reused = true;
236
+ summary.wallet.address = addressForKey(key);
237
+ say(`\nStep 1/3 — Seller wallet: reusing the existing key${summary.wallet.address ? ` for ${summary.wallet.address}` : ''}.`);
238
+ } else if (!flags.noWallet && chosen) {
239
+ const go = await ask('\nStep 1/3 — Seller wallet.\nGenerate one now? The private key is written straight into your MCP client\nconfig (mode 0600) and is never printed here. Selling needs no gas and no\nfunds. [y/N] ');
240
+ if (go) {
241
+ const w = generateWallet();
242
+ key = w.privateKey;
243
+ summary.wallet.generated = true;
244
+ summary.wallet.address = w.address;
245
+ say(`Generated a seller wallet: ${w.address}\nThe key is NOT printed — step 2 writes it into your client config.`);
190
246
  } else {
191
- const gen = await rl.question('\nNo seller key found. Generate one now (prints once to this terminal)? [y/N] ');
192
- if (/^y(es)?$/i.test(gen.trim())) {
193
- rl.close();
194
- console.log('\nGenerating seller wallet…\n');
195
- const kg = spawnSync(process.execPath, [path.join(__dirname, 'keygen.js')], { stdio: 'inherit' });
196
- if (kg.status !== 0) console.error('datum-onboard: keygen failed — run `npx datum-keygen` manually.');
247
+ say('Skipped. Selling stays off until a key is set — run `npx datum-keygen` when ready.');
248
+ }
249
+ } else if (!flags.noWallet && !chosen) {
250
+ say('\nStep 1/3 — Seller wallet: no writable MCP client config found, so no key was\ncreated. Run `npx datum-keygen` and paste the key into the block shown below.');
251
+ }
252
+
253
+ // ---------------------------------------------------------------- step 2/3
254
+ if (chosen) {
255
+ const entry = mcpClient.serverEntry({ key, cap });
256
+ const plan = mcpClient.planConfig(chosen.file, chosen.root, mcpClient.SERVER_NAME, entry);
257
+ if (plan.action === 'error') {
258
+ summary.client.action = 'error';
259
+ say(`\nStep 2/3 — Connect: not registered — ${plan.error}`);
260
+ } else {
261
+ summary.client.action = plan.action;
262
+ if (plan.action === 'unchanged') {
263
+ say(`\nStep 2/3 — Connect: ${chosen.label} already has the Datum server registered. Nothing to change.`);
197
264
  } else {
198
- rl.close();
199
- console.log('\nSkipped. Run `npx datum-keygen` when you are ready — selling needs no gas.');
265
+ const go = await ask(`\nStep 2/3 — Connect to ${chosen.label}.\nRegister the Datum MCP server in:\n ${chosen.file}\nThis is what gives your agent the datum tools. Proceed? [y/N] `);
266
+ if (!go) {
267
+ say('Skipped — the server was not registered.');
268
+ } else if (flags.dryRun) {
269
+ say('(dry run — nothing written)');
270
+ } else {
271
+ summary.client.wrote = writeFileSafe(chosen.file, plan.content);
272
+ if (summary.client.wrote) say(`Registered (${plan.action}). Backup: ${chosen.file}.datum-bak`);
273
+ }
200
274
  }
201
275
  }
276
+ } else if (!flags.noConnect) {
277
+ say('\nStep 2/3 — Connect: no known MCP client config was found on this machine.\nAdd this to your MCP client config, replacing the key:\n');
278
+ say(mcpClient.pasteBlock({ cap }));
279
+ say('\n(Generate that key with `npx datum-keygen`.)');
202
280
  }
203
281
 
204
- const changed = writeFileSafe(target.file, merged.content);
205
- summary.wrote = changed;
206
-
207
- if (flags.json) {
208
- console.log(JSON.stringify(summary, null, 2));
209
- return;
282
+ // ---------------------------------------------------------------- step 3/3
283
+ if (policyPlan.action === 'unchanged') {
284
+ say(`\nStep 3/3 — Policy: already current in ${target.file}. Nothing to do.`);
285
+ } else {
286
+ const verb = policyPlan.action === 'added' ? 'Add' : 'Update';
287
+ const go = await ask(`\nStep 3/3 — Standing policy.\n${verb} the Datum every-task policy in:\n ${target.file}\nProceed? [y/N] `);
288
+ if (!go) {
289
+ say('Skipped — the policy was not installed.');
290
+ } else if (flags.dryRun) {
291
+ say('(dry run — nothing written)');
292
+ } else {
293
+ summary.policy.wrote = writeFileSafe(target.file, policyPlan.content);
294
+ if (summary.policy.wrote) say(`Written. Backup: ${target.file}.datum-bak`);
295
+ }
210
296
  }
211
297
 
212
- console.log(`\n${changed ? (merged.action === 'added' ? 'Added' : 'Updated') : 'Unchanged'}: ${target.file}`);
213
- if (changed) console.log(`Backup (first change only): ${target.file}.datum-bak`);
214
- console.log('\nYour agent is good to go. The policy loads the next time it starts a session.');
215
- console.log('It will search Datum before collecting data, and list what it produces after every task.');
216
- if (!summary.keyPresent) {
217
- console.log('\nTo enable buying and selling, set DATUM_SIGNER_KEY in your MCP client env block');
218
- console.log('(run `npx datum-keygen` if you have not). Keep the key out of chat and out of this file.');
298
+ if (rl) rl.close();
299
+
300
+ if (flags.json) { console.log(JSON.stringify(summary, null, 2)); return; }
301
+
302
+ const connected = summary.client && (summary.client.wrote || summary.client.action === 'unchanged');
303
+ say('');
304
+ if (summary.wallet.generated) {
305
+ say('Back up your seller key: it lives in the config file above (mode 0600), and');
306
+ say('whoever holds it controls the DTM your listings earn.');
307
+ }
308
+ if (connected && summary.policy.wrote) {
309
+ say('Your agent is good to go. The policy loads the next time it starts a session:');
310
+ say('it will search Datum before collecting data, and list what it produces after');
311
+ say('every task — no marketplace wording needed in your prompts.');
312
+ } else if (connected) {
313
+ say('Server connected. Run again (or install the policy) to finish the setup.');
314
+ } else if (summary.policy.wrote) {
315
+ say('Policy installed. Connect the MCP server (step 2) to enable the datum tools.');
219
316
  }
317
+ if (flags.dryRun) say('\n(dry run — nothing was written)');
220
318
  }
221
319
 
222
320
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datum-mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Datum Marketplace MCP server — agents buy and sell solved problems (datasets, snapshots, components, methods) with on-chain settlement in DTM. Ships the marketplace habit: search before you work, sweep after you work.",
5
5
  "main": "server.js",
6
6
  "bin": { "datum-mcp": "server.js", "datum-keygen": "keygen.js", "datum-onboard": "onboard.js" },