@tacitai/mcp-server 0.4.0 → 0.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/dist/prompts.js CHANGED
@@ -1,10 +1,51 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import { z } from 'zod';
3
- const ASSISTANT_SYSTEM_PROMPT = "You are TACIT's workspace assistant. Answer from the user's workspace through TACIT tools. " +
3
+ const ASSISTANT_SYSTEM_PROMPT_STATIC = "You are TACIT's workspace assistant. Answer from the user's workspace through TACIT tools. " +
4
4
  'Prefer a single read tool for direct lookups, synthesis for open questions, and ask before any write or dispatch.';
5
5
  function userText(text) {
6
6
  return { messages: [{ role: 'user', content: { type: 'text', text } }] };
7
7
  }
8
+ function buildAssistantSystemPrompt(state) {
9
+ const name = state?.name || 'this workspace';
10
+ const lines = [];
11
+ lines.push(`You are the workspace assistant for ${name}. You help the team find, organize, and act on their business knowledge through natural language.`);
12
+ lines.push('');
13
+ lines.push('## Your personality');
14
+ lines.push('');
15
+ lines.push(`- Warm but direct. You know ${name} well and you are here to help.`);
16
+ lines.push('- Refer to the workspace by name. When citing answers, name the collection or source they came from.');
17
+ lines.push('- Never list tool names, API endpoints, or technical identifiers. Translate everything into plain language.');
18
+ lines.push('- Celebrate when something works — say which collection or source an answer came from rather than returning a silent result.');
19
+ lines.push('');
20
+ lines.push('## How you work');
21
+ lines.push('');
22
+ lines.push('- Questions about the business → search and synthesize from workspace knowledge, always citing sources.');
23
+ lines.push('- Requests to add content → ingest URLs, files, or text into the workspace.');
24
+ lines.push('- Organizing work → create collections, portfolios, programs, dossiers — whatever structure the user describes.');
25
+ lines.push('- Daily operations → morning briefs (with email triage and Slack synthesis when connected), meeting prep, draft replies.');
26
+ lines.push('- Business workflows → sales proposals, research and publishing, audits, weekly reviews.');
27
+ lines.push('- Delegation → dispatch crews for complex deliverables; nothing executes without the user\'s approval.');
28
+ lines.push('');
29
+ lines.push('## Progressive disclosure');
30
+ lines.push('');
31
+ lines.push('Introduce capabilities as they become relevant, not all at once. A user asking their first question does not need to hear about crew delegation. A user building their third report is ready to learn about voice profiles.');
32
+ lines.push('');
33
+ lines.push('When suggesting what to try next, frame it around what they just accomplished:');
34
+ lines.push('- After their first search: "Now that you have seen what is here — want to add something new?"');
35
+ lines.push('- After ingesting content: "Good — that is in your workspace now. Want to organize it into a collection?"');
36
+ lines.push('- After connecting tools: "Your Slack and email are flowing in. Try asking for a morning brief."');
37
+ lines.push('');
38
+ lines.push('## Safety');
39
+ lines.push('');
40
+ lines.push('- Never modify or delete content without explicit approval.');
41
+ lines.push('- Crew tasks require the user to approve before execution.');
42
+ lines.push('- When unsure about a destructive action, ask first.');
43
+ lines.push('');
44
+ lines.push('## Technical routing');
45
+ lines.push('');
46
+ lines.push('Prefer a single read tool for direct lookups, synthesis for open questions, and ask before any write or dispatch.');
47
+ return lines.join('\n');
48
+ }
8
49
  async function fetchWorkspaceState(client) {
9
50
  await client.initialize();
10
51
  const wsName = client.workspaceName || 'your workspace';
@@ -63,7 +104,19 @@ const EXPERIENCES = [
63
104
  { id: 'operating_rhythm', num: 9, name: 'Your Operating Rhythm', tier: 3, tierName: 'Agentic Mastery', action: 'Set up weekly reviews, delegate recurring work to crews, and build your voice profile.', signal: 'Return for second session with intent; establish a routine' },
64
105
  ];
65
106
  export function registerPrompts(server, getClientFn) {
66
- server.prompt('assistant_system', 'System prompt for the TACIT workspace assistant.', () => userText(ASSISTANT_SYSTEM_PROMPT));
107
+ server.prompt('assistant_system', 'System prompt for the TACIT workspace assistant — includes persona, progressive disclosure, and safety rules.', async () => {
108
+ if (getClientFn) {
109
+ try {
110
+ const client = await getClientFn();
111
+ const state = await fetchWorkspaceState(client);
112
+ return userText(buildAssistantSystemPrompt(state));
113
+ }
114
+ catch {
115
+ // fall through to static prompt
116
+ }
117
+ }
118
+ return userText(ASSISTANT_SYSTEM_PROMPT_STATIC);
119
+ });
67
120
  server.prompt('lookup', 'Fetch a specific item or list using a single TACIT read tool.', { request: z.string().describe('What to look up, e.g. "list programs"') }, ({ request }) => userText(`Use exactly one TACIT read tool to answer, without synthesis. Request: ${request}`));
68
121
  server.prompt('synthesis', 'Answer a question with grounded synthesis and citations.', { question: z.string().describe('The question to answer') }, ({ question }) => userText(`Call tacit_synthesize for this question and cite the sources it returns. Question: ${question}`));
69
122
  if (!getClientFn)
package/dist/setup.js CHANGED
@@ -33,6 +33,7 @@ import * as path from 'path';
33
33
  import * as os from 'os';
34
34
  import * as http from 'http';
35
35
  import * as crypto from 'crypto';
36
+ import * as readline from 'readline';
36
37
  import { execFile } from 'child_process';
37
38
  import { TacitClient, TacitApiError } from './client.js';
38
39
  function parseArgs(argv) {
@@ -314,6 +315,81 @@ async function deviceAuthFlow(apiUrl) {
314
315
  });
315
316
  });
316
317
  }
318
+ function prompt(question, hidden = false) {
319
+ return new Promise((resolve) => {
320
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
321
+ if (hidden && process.stdin.isTTY) {
322
+ process.stderr.write(question);
323
+ const stdin = process.stdin;
324
+ stdin.setRawMode(true);
325
+ stdin.resume();
326
+ stdin.setEncoding('utf-8');
327
+ let input = '';
328
+ const onData = (ch) => {
329
+ if (ch === '\n' || ch === '\r' || ch === '\u0004') {
330
+ stdin.setRawMode(false);
331
+ stdin.pause();
332
+ stdin.removeListener('data', onData);
333
+ rl.close();
334
+ process.stderr.write('\n');
335
+ resolve(input);
336
+ }
337
+ else if (ch === '\u007F' || ch === '\b') {
338
+ if (input.length > 0)
339
+ input = input.slice(0, -1);
340
+ }
341
+ else if (ch === '\u0003') {
342
+ process.exit(1);
343
+ }
344
+ else {
345
+ input += ch;
346
+ }
347
+ };
348
+ stdin.on('data', onData);
349
+ }
350
+ else {
351
+ rl.question(question, (answer) => {
352
+ rl.close();
353
+ resolve(answer.trim());
354
+ });
355
+ }
356
+ });
357
+ }
358
+ async function loginFlow(apiUrl) {
359
+ process.stderr.write('\n Log in with your TACIT credentials.\n\n');
360
+ const email = await prompt(' Email: ');
361
+ const pin = await prompt(' PIN: ', true);
362
+ if (!email || !pin) {
363
+ process.stderr.write('Error: email and PIN are required.\n');
364
+ process.exit(1);
365
+ }
366
+ const loginUrl = apiUrl.endsWith('/api/v1')
367
+ ? `${apiUrl}/auth/login`
368
+ : `${apiUrl}/api/v1/auth/login`;
369
+ const res = await fetch(loginUrl, {
370
+ method: 'POST',
371
+ headers: { 'Content-Type': 'application/json' },
372
+ body: JSON.stringify({ email, pin }),
373
+ });
374
+ if (!res.ok) {
375
+ const body = await res.text().catch(() => '');
376
+ if (res.status === 401) {
377
+ process.stderr.write('\n Login failed — check your email and PIN.\n');
378
+ }
379
+ else {
380
+ process.stderr.write(`\n Login failed (${res.status}): ${body}\n`);
381
+ }
382
+ process.exit(1);
383
+ }
384
+ const data = await res.json();
385
+ const token = typeof data['session_token'] === 'string' ? data['session_token'] : '';
386
+ const workspaceName = typeof data['workspace_name'] === 'string' ? data['workspace_name'] : '';
387
+ if (!token) {
388
+ process.stderr.write('Error: login returned no session token.\n');
389
+ process.exit(1);
390
+ }
391
+ return { token, workspaceName };
392
+ }
317
393
  // ─── Smoke synthesis ──────────────────────────────────────────────────────────
318
394
  const SMOKE_QUERY = 'What is in this knowledge base?';
319
395
  async function runSmoke(apiUrl, token) {
@@ -348,15 +424,8 @@ async function runSmoke(apiUrl, token) {
348
424
  // ─── Main export ──────────────────────────────────────────────────────────────
349
425
  export async function setup(argv) {
350
426
  const opts = parseArgs(argv);
351
- // Resolve api-url: flag > env.
352
- const apiUrl = opts.apiUrl ?? process.env['TACIT_API_URL'] ?? null;
353
- if (!apiUrl) {
354
- process.stderr.write('Error: no API URL configured.\n' +
355
- 'Provide --api-url <url> or set TACIT_API_URL environment variable.\n' +
356
- 'Example: npx @tacitai/mcp-server setup --api-url https://tacit-beta-prod.fly.dev --token <your-token>\n' +
357
- '\nGet your token from: https://beta.tacitai.app → Settings → Bearer token\n');
358
- process.exit(1);
359
- }
427
+ // Resolve api-url: flag > env > default.
428
+ const apiUrl = opts.apiUrl ?? process.env['TACIT_API_URL'] ?? 'https://beta.tacitai.app';
360
429
  // Determine profile and token.
361
430
  let profile = opts.profile;
362
431
  let token = opts.token;
@@ -365,23 +434,27 @@ export async function setup(argv) {
365
434
  const envVarNameForProfile = profileToEnvVar(profile);
366
435
  const envToken = process.env[envVarNameForProfile] ?? process.env['TACIT_TOKEN'] ?? null;
367
436
  if (!token && !envToken && !opts.dryRun) {
368
- // Neither --token nor env set and not a dry-run → run automated device-auth flow.
369
- let deviceResult;
437
+ // Neither --token nor env set and not a dry-run → authenticate interactively.
438
+ // Try device-auth (browser flow) first; fall back to login prompt.
439
+ let authenticated = false;
370
440
  try {
371
- deviceResult = await deviceAuthFlow(apiUrl);
441
+ const deviceResult = await deviceAuthFlow(apiUrl);
442
+ token = deviceResult.token;
443
+ expiresAt = deviceResult.expiresAt;
444
+ if (!opts.profileExplicit && deviceResult.workspaceName) {
445
+ profile = workspaceNameToProfile(deviceResult.workspaceName);
446
+ }
447
+ authenticated = true;
372
448
  }
373
- catch (err) {
374
- const msg = err instanceof Error ? err.message : String(err);
375
- process.stderr.write(`Error during device authentication: ${msg}\n`);
376
- process.exit(1);
377
- return; // unreachable; satisfies TypeScript
449
+ catch {
450
+ // Device-auth not available — fall back to login prompt.
378
451
  }
379
- // Token from device flow — use in memory only (NEVER log/write).
380
- token = deviceResult.token;
381
- expiresAt = deviceResult.expiresAt;
382
- // AC-12: derive profile from workspace_name unless --profile was explicit.
383
- if (!opts.profileExplicit && deviceResult.workspaceName) {
384
- profile = workspaceNameToProfile(deviceResult.workspaceName);
452
+ if (!authenticated) {
453
+ const loginResult = await loginFlow(apiUrl);
454
+ token = loginResult.token;
455
+ if (!opts.profileExplicit && loginResult.workspaceName) {
456
+ profile = workspaceNameToProfile(loginResult.workspaceName);
457
+ }
385
458
  }
386
459
  }
387
460
  // Final env-var name derived from (possibly updated) profile.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacitai/mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "stdio MCP server wrapping Tacit /api/v1 REST/SSE API",
5
5
  "license": "MIT",
6
6
  "repository": {