@tacitai/mcp-server 0.1.1 → 0.3.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/dist/auth.d.ts +32 -0
- package/dist/auth.js +64 -0
- package/dist/client.d.ts +60 -5
- package/dist/client.js +234 -53
- package/dist/index.js +130 -106
- package/dist/oauth.d.ts +115 -0
- package/dist/oauth.js +619 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +12 -0
- package/dist/setup.d.ts +28 -5
- package/dist/setup.js +266 -59
- package/dist/tools/agents.d.ts +277 -0
- package/dist/tools/agents.js +335 -0
- package/dist/tools/annotations.d.ts +117 -0
- package/dist/tools/annotations.js +130 -0
- package/dist/tools/artifacts.d.ts +178 -8
- package/dist/tools/artifacts.js +372 -81
- package/dist/tools/chronicle.d.ts +3 -0
- package/dist/tools/chronicle.js +165 -0
- package/dist/tools/connector_write_back.d.ts +3 -0
- package/dist/tools/connector_write_back.js +27 -0
- package/dist/tools/converse.d.ts +10 -0
- package/dist/tools/converse.js +88 -0
- package/dist/tools/crews.d.ts +3 -0
- package/dist/tools/crews.js +115 -0
- package/dist/tools/drafts.d.ts +83 -0
- package/dist/tools/drafts.js +86 -0
- package/dist/tools/errors.d.ts +15 -0
- package/dist/tools/errors.js +21 -0
- package/dist/tools/export.d.ts +54 -0
- package/dist/tools/export.js +55 -0
- package/dist/tools/gm.d.ts +214 -0
- package/dist/tools/gm.js +198 -0
- package/dist/tools/ingest.d.ts +58 -4
- package/dist/tools/ingest.js +109 -76
- package/dist/tools/kb.d.ts +544 -0
- package/dist/tools/kb.js +687 -0
- package/dist/tools/lake-structure.d.ts +3 -0
- package/dist/tools/lake-structure.js +126 -0
- package/dist/tools/lake.d.ts +932 -0
- package/dist/tools/lake.js +1017 -0
- package/dist/tools/move.d.ts +200 -0
- package/dist/tools/move.js +96 -0
- package/dist/tools/outbox.d.ts +274 -0
- package/dist/tools/outbox.js +223 -0
- package/dist/tools/people.d.ts +3 -0
- package/dist/tools/people.js +59 -0
- package/dist/tools/pyramid.d.ts +554 -0
- package/dist/tools/pyramid.js +758 -0
- package/dist/tools/recall.d.ts +2 -0
- package/dist/tools/recall.js +28 -17
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.js +199 -0
- package/dist/tools/research.d.ts +47 -0
- package/dist/tools/research.js +575 -0
- package/dist/tools/search.d.ts +9 -0
- package/dist/tools/search.js +86 -25
- package/dist/tools/sharing.d.ts +63 -0
- package/dist/tools/sharing.js +157 -0
- package/dist/tools/skills.d.ts +9 -0
- package/dist/tools/skills.js +98 -0
- package/dist/tools/synthesize.d.ts +54 -2
- package/dist/tools/synthesize.js +80 -23
- package/dist/tools/web-url.d.ts +57 -0
- package/dist/tools/web-url.js +74 -0
- package/dist/tools/workspaces.d.ts +3 -0
- package/dist/tools/workspaces.js +47 -0
- package/dist/transport.d.ts +9 -0
- package/dist/transport.js +209 -0
- package/package.json +5 -4
package/dist/setup.js
CHANGED
|
@@ -1,67 +1,45 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// SPDX-License-Identifier: MIT
|
|
3
2
|
/**
|
|
4
3
|
* setup — wizard subcommand for @tacit/mcp-server
|
|
5
4
|
*
|
|
6
5
|
* Usage:
|
|
7
|
-
* node dist/index.js setup [--dry-run] [--api-url <url>] [--token <token>] [--smoke]
|
|
6
|
+
* node dist/index.js setup [--dry-run] [--api-url <url>] [--token <token>] [--profile <name>] [--smoke]
|
|
8
7
|
*
|
|
9
|
-
* Contract (§2.1 of sparc_specification.md, Phase-2 QE resolutions):
|
|
8
|
+
* Contract (§2.1 of sparc_specification.md, Phase-2 QE resolutions + W5 device-auth):
|
|
10
9
|
* F1 smoke — calls tacit_synthesize via getClient()/postSSE; exits non-zero on empty/error.
|
|
11
|
-
* F2 env-ref — writes
|
|
12
|
-
* Merge — deep-merges ONLY mcpServers
|
|
13
|
-
* Idempotent — same --api-url → byte-identical mcp.json.
|
|
10
|
+
* F2 env-ref — writes TACIT_TOKEN_* as the LITERAL placeholder "${TACIT_TOKEN_*}" (never the real value).
|
|
11
|
+
* Merge — deep-merges ONLY mcpServers[profile]; all other keys preserved.
|
|
12
|
+
* Idempotent — same --api-url + --profile → byte-identical mcp.json.
|
|
14
13
|
* Secrets — token NEVER written to any output stream.
|
|
15
14
|
*
|
|
15
|
+
* W5 additions:
|
|
16
|
+
* --profile <name> — profile name (default: 'tacit'); drives env-var name + mcp.json key.
|
|
17
|
+
* Device-auth flow — when --token is NOT provided and token env var is absent, opens browser
|
|
18
|
+
* to ${apiUrl}/auth/device, waits for localhost:9123 callback, exchanges
|
|
19
|
+
* code for token, derives profile from workspace_name (unless --profile set).
|
|
20
|
+
* Metadata file — writes ~/.tacit/profiles/<profile>.json with { expires_at, api_url }.
|
|
21
|
+
*
|
|
16
22
|
* Security invariants:
|
|
17
23
|
* - Token is NEVER logged or written to disk.
|
|
18
24
|
* - Authorization header value excluded from all logging paths.
|
|
25
|
+
* - State nonce generated from crypto.randomBytes (never user-provided).
|
|
26
|
+
* - Tampered state in device-auth callback → exit 1 (S-10/AC-13).
|
|
19
27
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}) : (function(o, m, k, k2) {
|
|
28
|
-
if (k2 === undefined) k2 = k;
|
|
29
|
-
o[k2] = m[k];
|
|
30
|
-
}));
|
|
31
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
-
}) : function(o, v) {
|
|
34
|
-
o["default"] = v;
|
|
35
|
-
});
|
|
36
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
-
var ownKeys = function(o) {
|
|
38
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
-
var ar = [];
|
|
40
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
-
return ar;
|
|
42
|
-
};
|
|
43
|
-
return ownKeys(o);
|
|
44
|
-
};
|
|
45
|
-
return function (mod) {
|
|
46
|
-
if (mod && mod.__esModule) return mod;
|
|
47
|
-
var result = {};
|
|
48
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
-
__setModuleDefault(result, mod);
|
|
50
|
-
return result;
|
|
51
|
-
};
|
|
52
|
-
})();
|
|
53
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.setup = setup;
|
|
55
|
-
const fs = __importStar(require("fs"));
|
|
56
|
-
const path = __importStar(require("path"));
|
|
57
|
-
const os = __importStar(require("os"));
|
|
58
|
-
const client_js_1 = require("./client.js");
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
import * as os from 'os';
|
|
31
|
+
import * as http from 'http';
|
|
32
|
+
import * as crypto from 'crypto';
|
|
33
|
+
import { execFile } from 'child_process';
|
|
34
|
+
import { TacitClient, TacitApiError } from './client.js';
|
|
59
35
|
function parseArgs(argv) {
|
|
60
36
|
const opts = {
|
|
61
37
|
dryRun: false,
|
|
62
38
|
apiUrl: null,
|
|
63
39
|
token: null,
|
|
64
40
|
smoke: false,
|
|
41
|
+
profile: 'tacit',
|
|
42
|
+
profileExplicit: false,
|
|
65
43
|
};
|
|
66
44
|
for (let i = 0; i < argv.length; i++) {
|
|
67
45
|
const arg = argv[i];
|
|
@@ -89,9 +67,44 @@ function parseArgs(argv) {
|
|
|
89
67
|
opts.token = next;
|
|
90
68
|
i++;
|
|
91
69
|
}
|
|
70
|
+
else if (arg === '--profile') {
|
|
71
|
+
const next = argv[i + 1];
|
|
72
|
+
if (!next || next.startsWith('--')) {
|
|
73
|
+
process.stderr.write('Error: --profile requires a value\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
opts.profile = next;
|
|
77
|
+
opts.profileExplicit = true;
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
92
80
|
}
|
|
93
81
|
return opts;
|
|
94
82
|
}
|
|
83
|
+
// ─── Profile / env-var helpers ────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Derive the environment variable name for a given profile name.
|
|
86
|
+
*
|
|
87
|
+
* AC-3, AC-4, AC-10:
|
|
88
|
+
* 'tacit' → 'TACIT_TOKEN'
|
|
89
|
+
* 'tacit-acme' → 'TACIT_TOKEN_ACME'
|
|
90
|
+
* 'my-workspace' → 'TACIT_TOKEN_MY_WORKSPACE'
|
|
91
|
+
*/
|
|
92
|
+
export function profileToEnvVar(profile) {
|
|
93
|
+
// Strip leading 'tacit-' prefix if present
|
|
94
|
+
const stripped = profile.startsWith('tacit-') ? profile.slice(6) : profile;
|
|
95
|
+
// Special case: default profile ('tacit') → stripped = '' → 'TACIT_TOKEN'
|
|
96
|
+
if (stripped === '' || stripped === 'tacit') {
|
|
97
|
+
return 'TACIT_TOKEN';
|
|
98
|
+
}
|
|
99
|
+
return 'TACIT_TOKEN_' + stripped.replace(/-/g, '_').toUpperCase();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Derive a profile name from a workspace_name returned by device-auth exchange.
|
|
103
|
+
* AC-12: lowercased, spaces→hyphens, non-alphanumeric-hyphen stripped.
|
|
104
|
+
*/
|
|
105
|
+
export function workspaceNameToProfile(name) {
|
|
106
|
+
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
107
|
+
}
|
|
95
108
|
function claudeDir() {
|
|
96
109
|
return path.join(os.homedir(), '.claude');
|
|
97
110
|
}
|
|
@@ -113,26 +126,32 @@ function readExistingConfig() {
|
|
|
113
126
|
return {};
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Build the MCP server entry for a given profile.
|
|
131
|
+
* The env-var placeholder is the LITERAL string "${TACIT_TOKEN_*}" — never the real value.
|
|
132
|
+
*/
|
|
133
|
+
function buildProfileEntry(apiUrl, envVarName) {
|
|
117
134
|
return {
|
|
118
135
|
command: 'npx',
|
|
119
136
|
args: ['-y', '@tacitai/mcp-server'],
|
|
120
137
|
env: {
|
|
121
138
|
// F2: literal placeholder — Claude Code expands ${VAR} at launch; real token NEVER on disk.
|
|
122
|
-
|
|
139
|
+
[envVarName]: `\${${envVarName}}`,
|
|
123
140
|
// TACIT_API_URL is a non-secret URL; written as-is.
|
|
124
141
|
TACIT_API_URL: apiUrl,
|
|
125
142
|
},
|
|
126
143
|
};
|
|
127
144
|
}
|
|
128
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Deep-merge ONLY the mcpServers[profileName] key; all other keys preserved.
|
|
147
|
+
*/
|
|
148
|
+
function mergeConfig(existing, profileName, entry) {
|
|
129
149
|
const servers = existing.mcpServers ?? {};
|
|
130
|
-
// Deep-merge ONLY tacit key; all other mcpServers entries and top-level keys preserved.
|
|
131
150
|
const merged = {
|
|
132
151
|
...existing,
|
|
133
152
|
mcpServers: {
|
|
134
153
|
...servers,
|
|
135
|
-
|
|
154
|
+
[profileName]: entry,
|
|
136
155
|
},
|
|
137
156
|
};
|
|
138
157
|
return merged;
|
|
@@ -140,10 +159,159 @@ function mergeConfig(existing, tacitEntry) {
|
|
|
140
159
|
function configToBytes(cfg) {
|
|
141
160
|
return JSON.stringify(cfg, null, 2) + '\n';
|
|
142
161
|
}
|
|
162
|
+
// ─── Metadata file ────────────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Write ~/.tacit/profiles/<profile>.json with { expires_at, api_url }.
|
|
165
|
+
* Token plaintext is NEVER written here — metadata only.
|
|
166
|
+
*/
|
|
167
|
+
function writeProfileMetadata(profile, expiresAt, apiUrl) {
|
|
168
|
+
const metaDir = path.join(os.homedir(), '.tacit', 'profiles');
|
|
169
|
+
fs.mkdirSync(metaDir, { recursive: true });
|
|
170
|
+
fs.writeFileSync(path.join(metaDir, `${profile}.json`), JSON.stringify({ expires_at: expiresAt, api_url: apiUrl }, null, 2));
|
|
171
|
+
}
|
|
172
|
+
// ─── Device-auth flow ─────────────────────────────────────────────────────────
|
|
173
|
+
const DEVICE_AUTH_CALLBACK_PORT = 9123;
|
|
174
|
+
const DEVICE_AUTH_TIMEOUT_MS = 120_000;
|
|
175
|
+
/**
|
|
176
|
+
* Opens a browser to the device-auth URL and waits for the OAuth callback.
|
|
177
|
+
* Returns the token, expiry, and workspace name from the exchange response.
|
|
178
|
+
*
|
|
179
|
+
* Security:
|
|
180
|
+
* - State nonce is generated via crypto.randomBytes (never user-provided).
|
|
181
|
+
* - Tampered state → exit 1 (S-10 / AC-13).
|
|
182
|
+
* - Token plaintext is NEVER logged.
|
|
183
|
+
*
|
|
184
|
+
* Error paths:
|
|
185
|
+
* - Port 9123 occupied → print error + --token fallback instructions → exit 1 (AC-15).
|
|
186
|
+
* - 120s timeout → exit 1 (AC-15).
|
|
187
|
+
* - Browser open fails → print URL to stdout for manual copy-paste (AC-16 fallback).
|
|
188
|
+
*/
|
|
189
|
+
async function deviceAuthFlow(apiUrl) {
|
|
190
|
+
// 1. Generate a cryptographically random state nonce (32 bytes, hex).
|
|
191
|
+
const stateNonce = crypto.randomBytes(32).toString('hex');
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
let server = null;
|
|
194
|
+
let timeoutHandle = null;
|
|
195
|
+
const cleanup = () => {
|
|
196
|
+
if (timeoutHandle)
|
|
197
|
+
clearTimeout(timeoutHandle);
|
|
198
|
+
if (server) {
|
|
199
|
+
server.close(() => { });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
// 2. Start localhost:9123 HTTP server for callback.
|
|
203
|
+
server = http.createServer(async (req, res) => {
|
|
204
|
+
const reqUrl = req.url ?? '';
|
|
205
|
+
if (!reqUrl.startsWith('/callback')) {
|
|
206
|
+
res.writeHead(404);
|
|
207
|
+
res.end('Not found');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const parsedUrl = new URL(reqUrl, `http://localhost:${DEVICE_AUTH_CALLBACK_PORT}`);
|
|
211
|
+
const returnedState = parsedUrl.searchParams.get('state') ?? '';
|
|
212
|
+
const code = parsedUrl.searchParams.get('code') ?? '';
|
|
213
|
+
// 6a. Verify state nonce — tampered state → exit 1 (S-10/AC-13).
|
|
214
|
+
if (returnedState !== stateNonce) {
|
|
215
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
216
|
+
res.end('Invalid state parameter.');
|
|
217
|
+
cleanup();
|
|
218
|
+
reject(new Error('CSRF state mismatch: state nonce did not match. Aborting device auth.'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!code) {
|
|
222
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
223
|
+
res.end('Missing code parameter.');
|
|
224
|
+
cleanup();
|
|
225
|
+
reject(new Error('Device auth callback missing code parameter.'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// 6c. Exchange code for token.
|
|
229
|
+
try {
|
|
230
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
231
|
+
res.end('<html><body><h1>Authentication complete. You may close this tab.</h1></body></html>');
|
|
232
|
+
const exchangeRes = await fetch(`${apiUrl}/api/v1/auth/device-exchange`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body: JSON.stringify({ code }),
|
|
236
|
+
});
|
|
237
|
+
if (!exchangeRes.ok) {
|
|
238
|
+
const errBody = await exchangeRes.text().catch(() => '');
|
|
239
|
+
cleanup();
|
|
240
|
+
reject(new Error(`Device auth exchange failed (${exchangeRes.status}): ${errBody}`));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const data = await exchangeRes.json();
|
|
244
|
+
const token = typeof data['token'] === 'string' ? data['token'] : '';
|
|
245
|
+
const expiresAt = typeof data['expires_at'] === 'string' ? data['expires_at'] : '';
|
|
246
|
+
const workspaceName = typeof data['workspace_name'] === 'string' ? data['workspace_name'] : 'tacit';
|
|
247
|
+
if (!token) {
|
|
248
|
+
cleanup();
|
|
249
|
+
reject(new Error('Device auth exchange returned empty token.'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
cleanup();
|
|
253
|
+
resolve({ token, expiresAt, workspaceName });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
cleanup();
|
|
257
|
+
reject(err);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
server.on('error', (err) => {
|
|
261
|
+
if (err.code === 'EADDRINUSE') {
|
|
262
|
+
process.stderr.write(`Error: port ${DEVICE_AUTH_CALLBACK_PORT} is already in use.\n` +
|
|
263
|
+
`Cannot start device-auth callback server.\n` +
|
|
264
|
+
`Use --token <your-token> instead:\n` +
|
|
265
|
+
` npx @tacitai/mcp-server setup --api-url ${apiUrl} --token <your-token>\n` +
|
|
266
|
+
`Get your token from: ${apiUrl}/settings\n`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
cleanup();
|
|
270
|
+
reject(err);
|
|
271
|
+
});
|
|
272
|
+
server.listen(DEVICE_AUTH_CALLBACK_PORT, '127.0.0.1', () => {
|
|
273
|
+
// 3. Build device URL.
|
|
274
|
+
const callbackUrl = `http://localhost:${DEVICE_AUTH_CALLBACK_PORT}/callback`;
|
|
275
|
+
const deviceUrl = `${apiUrl}/auth/device?callback=${encodeURIComponent(callbackUrl)}&state=${stateNonce}`;
|
|
276
|
+
// 4. Try to open the browser.
|
|
277
|
+
// Use execFile with an argument array (not exec) to avoid shell injection
|
|
278
|
+
// — deviceUrl is built from user-supplied --api-url and must not be
|
|
279
|
+
// interpolated into a shell command string.
|
|
280
|
+
const platform = process.platform;
|
|
281
|
+
const [openBin, openArgs] = platform === 'win32'
|
|
282
|
+
? ['cmd', ['/c', 'start', '', deviceUrl]]
|
|
283
|
+
: platform === 'darwin'
|
|
284
|
+
? ['open', [deviceUrl]]
|
|
285
|
+
: ['xdg-open', [deviceUrl]];
|
|
286
|
+
execFile(openBin, openArgs, (err) => {
|
|
287
|
+
if (err) {
|
|
288
|
+
// AC-16 fallback: print URL for manual copy-paste.
|
|
289
|
+
process.stdout.write(`Could not open browser automatically.\n` +
|
|
290
|
+
`Open this URL in your browser to authenticate:\n` +
|
|
291
|
+
` ${deviceUrl}\n`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
process.stdout.write(`Opening browser for authentication...\n` +
|
|
295
|
+
`If the browser does not open, visit:\n` +
|
|
296
|
+
` ${deviceUrl}\n`);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
// 5. Timeout guard (AC-15).
|
|
300
|
+
timeoutHandle = setTimeout(() => {
|
|
301
|
+
cleanup();
|
|
302
|
+
process.stderr.write(`Error: device-auth timed out after ${DEVICE_AUTH_TIMEOUT_MS / 1000}s.\n` +
|
|
303
|
+
`Use --token <your-token> instead:\n` +
|
|
304
|
+
` npx @tacitai/mcp-server setup --api-url ${apiUrl} --token <your-token>\n` +
|
|
305
|
+
`Get your token from: ${apiUrl}/settings\n`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}, DEVICE_AUTH_TIMEOUT_MS);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
143
311
|
// ─── Smoke synthesis ──────────────────────────────────────────────────────────
|
|
144
312
|
const SMOKE_QUERY = 'What is in this knowledge base?';
|
|
145
313
|
async function runSmoke(apiUrl, token) {
|
|
146
|
-
const client = new
|
|
314
|
+
const client = new TacitClient({
|
|
147
315
|
apiUrl,
|
|
148
316
|
token: token ?? process.env['TACIT_TOKEN'] ?? '',
|
|
149
317
|
});
|
|
@@ -157,7 +325,7 @@ async function runSmoke(apiUrl, token) {
|
|
|
157
325
|
}
|
|
158
326
|
}
|
|
159
327
|
catch (err) {
|
|
160
|
-
const msg = err instanceof
|
|
328
|
+
const msg = err instanceof TacitApiError
|
|
161
329
|
? `API error ${err.statusCode}: ${err.body}`
|
|
162
330
|
: err instanceof Error ? err.message : String(err);
|
|
163
331
|
process.stderr.write(`SMOKE_SYNTH_FAIL: ${msg}\n`);
|
|
@@ -172,23 +340,52 @@ async function runSmoke(apiUrl, token) {
|
|
|
172
340
|
process.exit(0);
|
|
173
341
|
}
|
|
174
342
|
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
175
|
-
async function setup(argv) {
|
|
343
|
+
export async function setup(argv) {
|
|
176
344
|
const opts = parseArgs(argv);
|
|
177
|
-
// Resolve api-url: flag > env.
|
|
345
|
+
// Resolve api-url: flag > env.
|
|
178
346
|
const apiUrl = opts.apiUrl ?? process.env['TACIT_API_URL'] ?? null;
|
|
179
|
-
const token = opts.token ?? process.env['TACIT_TOKEN'] ?? null;
|
|
180
347
|
if (!apiUrl) {
|
|
181
348
|
process.stderr.write('Error: no API URL configured.\n' +
|
|
182
349
|
'Provide --api-url <url> or set TACIT_API_URL environment variable.\n' +
|
|
183
|
-
'Example:
|
|
350
|
+
'Example: npx @tacitai/mcp-server setup --api-url https://tacit-beta-prod.fly.dev --token <your-token>\n' +
|
|
351
|
+
'\nGet your token from: https://beta.tacitai.app → Settings → Bearer token\n');
|
|
184
352
|
process.exit(1);
|
|
185
353
|
}
|
|
186
|
-
|
|
354
|
+
// Determine profile and token.
|
|
355
|
+
let profile = opts.profile;
|
|
356
|
+
let token = opts.token;
|
|
357
|
+
let expiresAt = '';
|
|
358
|
+
// Check env var for this profile (to support existing token path).
|
|
359
|
+
const envVarNameForProfile = profileToEnvVar(profile);
|
|
360
|
+
const envToken = process.env[envVarNameForProfile] ?? process.env['TACIT_TOKEN'] ?? null;
|
|
361
|
+
if (!token && !envToken && !opts.dryRun) {
|
|
362
|
+
// Neither --token nor env set and not a dry-run → run automated device-auth flow.
|
|
363
|
+
let deviceResult;
|
|
364
|
+
try {
|
|
365
|
+
deviceResult = await deviceAuthFlow(apiUrl);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
369
|
+
process.stderr.write(`Error during device authentication: ${msg}\n`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
return; // unreachable; satisfies TypeScript
|
|
372
|
+
}
|
|
373
|
+
// Token from device flow — use in memory only (NEVER log/write).
|
|
374
|
+
token = deviceResult.token;
|
|
375
|
+
expiresAt = deviceResult.expiresAt;
|
|
376
|
+
// AC-12: derive profile from workspace_name unless --profile was explicit.
|
|
377
|
+
if (!opts.profileExplicit && deviceResult.workspaceName) {
|
|
378
|
+
profile = workspaceNameToProfile(deviceResult.workspaceName);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Final env-var name derived from (possibly updated) profile.
|
|
382
|
+
const envVarName = profileToEnvVar(profile);
|
|
383
|
+
const profileEntry = buildProfileEntry(apiUrl, envVarName);
|
|
187
384
|
const existing = readExistingConfig();
|
|
188
|
-
const merged = mergeConfig(existing,
|
|
385
|
+
const merged = mergeConfig(existing, profile, profileEntry);
|
|
189
386
|
const output = configToBytes(merged);
|
|
190
387
|
// D10: emit confirmation string BEFORE writing (also printed in dry-run).
|
|
191
|
-
process.stdout.write(
|
|
388
|
+
process.stdout.write(`Writing tacit entry to ~/.claude/mcp.json (profile: ${profile})...\n`);
|
|
192
389
|
if (opts.dryRun) {
|
|
193
390
|
process.stdout.write('(dry-run) Would write:\n');
|
|
194
391
|
process.stdout.write(output);
|
|
@@ -202,6 +399,16 @@ async function setup(argv) {
|
|
|
202
399
|
}
|
|
203
400
|
const dest = mcpJsonPath();
|
|
204
401
|
fs.writeFileSync(dest, output, { encoding: 'utf-8', mode: 0o600 });
|
|
402
|
+
// AC-12: write ~/.tacit/profiles/<profile>.json metadata (token NOT included).
|
|
403
|
+
if (expiresAt) {
|
|
404
|
+
writeProfileMetadata(profile, expiresAt, apiUrl);
|
|
405
|
+
}
|
|
406
|
+
// AC-11: print export snippet to stdout so user can set the env var.
|
|
407
|
+
// Token itself is NOT printed here — user must have received it through the device flow.
|
|
408
|
+
process.stdout.write(`\nSetup complete. Add this to your shell profile:\n` +
|
|
409
|
+
` export ${envVarName}=<your-token>\n` +
|
|
410
|
+
`\nOr for this session:\n` +
|
|
411
|
+
` export ${envVarName}=<your-token>\n`);
|
|
205
412
|
if (opts.smoke) {
|
|
206
413
|
await runSmoke(apiUrl, token);
|
|
207
414
|
// runSmoke calls process.exit internally; return here for type safety.
|