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.
- package/onboard/client.js +194 -0
- package/onboard.js +190 -92
- package/package.json +1 -1
|
@@ -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 —
|
|
4
|
+
* datum-onboard — set an agent up on Datum in one command.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
* *
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* *
|
|
16
|
-
*
|
|
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
|
|
20
|
-
* npx datum-onboard --
|
|
21
|
-
* npx datum-onboard --
|
|
22
|
-
* npx datum-onboard --
|
|
23
|
-
* npx datum-onboard --
|
|
24
|
-
* npx datum-onboard --target
|
|
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
|
|
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 = {
|
|
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} —
|
|
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
|
|
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>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
--
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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.
|
|
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" },
|