@tacitai/mcp-server 0.3.0 → 0.3.2

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/client.d.ts CHANGED
@@ -72,7 +72,7 @@ export declare class TacitClient {
72
72
  /** GET request returning parsed JSON. */
73
73
  get<T = unknown>(endpoint: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
74
74
  /** POST request returning parsed JSON. `extraHeaders` merges on top of auth headers (e.g. If-Match). */
75
- post<T = unknown>(endpoint: string, body: unknown, extraHeaders?: Record<string, string>): Promise<T>;
75
+ post<T = unknown>(endpoint: string, body: unknown, extraHeaders?: Record<string, string>, timeoutMs?: number): Promise<T>;
76
76
  /** PUT request returning parsed JSON. */
77
77
  put<T = unknown>(endpoint: string, body: unknown): Promise<T>;
78
78
  /** DELETE request returning parsed JSON (or empty on 204). */
package/dist/client.js CHANGED
@@ -232,12 +232,15 @@ export class TacitClient {
232
232
  return res.json();
233
233
  }
234
234
  /** POST request returning parsed JSON. `extraHeaders` merges on top of auth headers (e.g. If-Match). */
235
- async post(endpoint, body, extraHeaders) {
236
- const res = await this._fetch(`${this.apiUrl}${endpoint}`, {
235
+ async post(endpoint, body, extraHeaders, timeoutMs) {
236
+ const opts = {
237
237
  method: 'POST',
238
238
  headers: this.authHeaders(extraHeaders),
239
239
  body: JSON.stringify(body),
240
- });
240
+ };
241
+ if (timeoutMs)
242
+ opts.signal = AbortSignal.timeout(timeoutMs);
243
+ const res = await this._fetch(`${this.apiUrl}${endpoint}`, opts);
241
244
  if (!res.ok) {
242
245
  const text = await res.text().catch(() => '');
243
246
  if (res.status === 401) {
package/dist/setup.d.ts CHANGED
@@ -6,10 +6,13 @@
6
6
  *
7
7
  * Contract (§2.1 of sparc_specification.md, Phase-2 QE resolutions + W5 device-auth):
8
8
  * F1 smoke — calls tacit_synthesize via getClient()/postSSE; exits non-zero on empty/error.
9
- * F2 env-ref — writes TACIT_TOKEN_* as the LITERAL placeholder "${TACIT_TOKEN_*}" (never the real value).
9
+ * F2 token — when token is obtained (device-auth or --token), writes it directly to mcp.json
10
+ * (file is 0600). Falls back to env-var placeholder "${TACIT_TOKEN_*}" when token
11
+ * came from the environment.
10
12
  * Merge — deep-merges ONLY mcpServers[profile]; all other keys preserved.
11
13
  * Idempotent — same --api-url + --profile → byte-identical mcp.json.
12
- * Secrets — token NEVER written to any output stream.
14
+ * Secrets — token written to mcp.json (0600) when obtained via device-auth or --token;
15
+ * NEVER printed to stdout/stderr (dry-run output is redacted).
13
16
  *
14
17
  * W5 additions:
15
18
  * --profile <name> — profile name (default: 'tacit'); drives env-var name + mcp.json key.
package/dist/setup.js CHANGED
@@ -7,10 +7,13 @@
7
7
  *
8
8
  * Contract (§2.1 of sparc_specification.md, Phase-2 QE resolutions + W5 device-auth):
9
9
  * F1 smoke — calls tacit_synthesize via getClient()/postSSE; exits non-zero on empty/error.
10
- * F2 env-ref — writes TACIT_TOKEN_* as the LITERAL placeholder "${TACIT_TOKEN_*}" (never the real value).
10
+ * F2 token — when token is obtained (device-auth or --token), writes it directly to mcp.json
11
+ * (file is 0600). Falls back to env-var placeholder "${TACIT_TOKEN_*}" when token
12
+ * came from the environment.
11
13
  * Merge — deep-merges ONLY mcpServers[profile]; all other keys preserved.
12
14
  * Idempotent — same --api-url + --profile → byte-identical mcp.json.
13
- * Secrets — token NEVER written to any output stream.
15
+ * Secrets — token written to mcp.json (0600) when obtained via device-auth or --token;
16
+ * NEVER printed to stdout/stderr (dry-run output is redacted).
14
17
  *
15
18
  * W5 additions:
16
19
  * --profile <name> — profile name (default: 'tacit'); drives env-var name + mcp.json key.
@@ -128,16 +131,19 @@ function readExistingConfig() {
128
131
  }
129
132
  /**
130
133
  * Build the MCP server entry for a given profile.
131
- * The env-var placeholder is the LITERAL string "${TACIT_TOKEN_*}" — never the real value.
134
+ *
135
+ * When `token` is provided (device-auth or --token), the actual token is written
136
+ * to mcp.json so the user doesn't need to set an env var. mcp.json is written
137
+ * with mode 0600 (owner-only), consistent with how CLI tools store auth tokens
138
+ * (e.g. ~/.npmrc). When `token` is null, the env-var placeholder is written
139
+ * so Claude Code expands ${VAR} at launch.
132
140
  */
133
- function buildProfileEntry(apiUrl, envVarName) {
141
+ function buildProfileEntry(apiUrl, envVarName, token) {
134
142
  return {
135
143
  command: 'npx',
136
144
  args: ['-y', '@tacitai/mcp-server'],
137
145
  env: {
138
- // F2: literal placeholder — Claude Code expands ${VAR} at launch; real token NEVER on disk.
139
- [envVarName]: `\${${envVarName}}`,
140
- // TACIT_API_URL is a non-secret URL; written as-is.
146
+ TACIT_TOKEN: token ?? `\${${envVarName}}`,
141
147
  TACIT_API_URL: apiUrl,
142
148
  },
143
149
  };
@@ -380,15 +386,22 @@ export async function setup(argv) {
380
386
  }
381
387
  // Final env-var name derived from (possibly updated) profile.
382
388
  const envVarName = profileToEnvVar(profile);
383
- const profileEntry = buildProfileEntry(apiUrl, envVarName);
389
+ // Pass the actual token when we have it (device-auth or --token);
390
+ // fall back to env-var placeholder when the token came from the environment.
391
+ const resolvedToken = token ?? null;
392
+ const profileEntry = buildProfileEntry(apiUrl, envVarName, resolvedToken);
384
393
  const existing = readExistingConfig();
385
394
  const merged = mergeConfig(existing, profile, profileEntry);
386
395
  const output = configToBytes(merged);
387
396
  // D10: emit confirmation string BEFORE writing (also printed in dry-run).
388
397
  process.stdout.write(`Writing tacit entry to ~/.claude/mcp.json (profile: ${profile})...\n`);
389
398
  if (opts.dryRun) {
399
+ // Redact any literal token in dry-run preview to avoid leaking secrets to stdout.
400
+ const redacted = resolvedToken
401
+ ? configToBytes(mergeConfig(existing, profile, buildProfileEntry(apiUrl, envVarName, null)))
402
+ : output;
390
403
  process.stdout.write('(dry-run) Would write:\n');
391
- process.stdout.write(output);
404
+ process.stdout.write(redacted);
392
405
  process.exit(0);
393
406
  return;
394
407
  }
@@ -399,16 +412,21 @@ export async function setup(argv) {
399
412
  }
400
413
  const dest = mcpJsonPath();
401
414
  fs.writeFileSync(dest, output, { encoding: 'utf-8', mode: 0o600 });
415
+ // Ensure 0600 even when the file pre-existed with looser permissions.
416
+ fs.chmodSync(dest, 0o600);
402
417
  // AC-12: write ~/.tacit/profiles/<profile>.json metadata (token NOT included).
403
418
  if (expiresAt) {
404
419
  writeProfileMetadata(profile, expiresAt, apiUrl);
405
420
  }
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`);
421
+ if (resolvedToken) {
422
+ process.stdout.write(`\nSetup complete. Your token has been saved to ~/.claude/mcp.json.\n` +
423
+ `Start a new Claude Code session to connect.\n`);
424
+ }
425
+ else {
426
+ process.stdout.write(`\nSetup complete. Add this to your shell profile:\n` +
427
+ ` export ${envVarName}=<your-token>\n` +
428
+ `\nThen start a new Claude Code session to connect.\n`);
429
+ }
412
430
  if (opts.smoke) {
413
431
  await runSmoke(apiUrl, token);
414
432
  // runSmoke calls process.exit internally; return here for type safety.
@@ -96,7 +96,7 @@ export function buildSynthesizeRequest(client, input) {
96
96
  }
97
97
  else {
98
98
  // JSON path: POST /api/v1/mind/synthesize
99
- const result = await client.post('/api/v1/mind/synthesize', body);
99
+ const result = await client.post('/api/v1/mind/synthesize', body, undefined, 120_000);
100
100
  return {
101
101
  content: [{ type: 'text', text: JSON.stringify(result) }],
102
102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacitai/mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "stdio MCP server wrapping Tacit /api/v1 REST/SSE API",
5
5
  "license": "MIT",
6
6
  "repository": {