@tacitai/mcp-server 0.6.2 → 0.6.4

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/index.js CHANGED
@@ -59,6 +59,7 @@ import { registerGmTools } from './tools/gm.js';
59
59
  import { registerSkillTools } from './tools/skills.js';
60
60
  import { registerWikiTools } from './tools/wiki.js';
61
61
  import { registerVoiceTools } from './tools/voice.js';
62
+ import { registerOnboardingTools } from './tools/onboarding.js';
62
63
  import { registerPrompts } from './prompts.js';
63
64
  import { getRequestPrincipal } from './client.js';
64
65
  // Setup wizard (MV-1057h / W5)
@@ -183,6 +184,7 @@ async function run() {
183
184
  registerSkillTools(server, getClient);
184
185
  registerWikiTools(server, getClient);
185
186
  registerVoiceTools(server, getClient);
187
+ registerOnboardingTools(server, getClient);
186
188
  registerPrompts(server, getClient);
187
189
  if (process.env['MCP_TRANSPORT'] === 'http') {
188
190
  await startHttpTransport(server);
package/dist/prompts.d.ts CHANGED
@@ -1,3 +1,90 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { TacitClient } from './client.js';
3
+ export interface WorkspaceState {
4
+ name: string;
5
+ collectionNames: string[];
6
+ collectionCount: number;
7
+ portfolioNames: string[];
8
+ portfolioCount: number;
9
+ sourceCount: number;
10
+ }
11
+ export declare function fetchWorkspaceState(client: TacitClient): Promise<WorkspaceState>;
12
+ export declare const CAPABILITY_DOMAINS: Array<{
13
+ domain: string;
14
+ triggers: string[];
15
+ description: string;
16
+ }>;
17
+ export declare const EXPERIENCES: readonly [{
18
+ readonly id: "first_light";
19
+ readonly num: 1;
20
+ readonly name: "First Light";
21
+ readonly tier: 1;
22
+ readonly tierName: "Orient & Equip";
23
+ readonly action: "Ask a question about your business — try \"What do we know about [topic]?\"";
24
+ readonly signal: "First grounded, cited answer about your company";
25
+ }, {
26
+ readonly id: "feed_it";
27
+ readonly num: 2;
28
+ readonly name: "Feed It";
29
+ readonly tier: 1;
30
+ readonly tierName: "Orient & Equip";
31
+ readonly action: "Add something to your workspace — share a URL, document, or file.";
32
+ readonly signal: "First successful content ingestion";
33
+ }, {
34
+ readonly id: "build_your_map";
35
+ readonly num: 3;
36
+ readonly name: "Build Your Map";
37
+ readonly tier: 1;
38
+ readonly tierName: "Orient & Equip";
39
+ readonly action: "Organize your work — ask to create a collection, build a portfolio, or structure your business.";
40
+ readonly signal: "First organizational write operation";
41
+ }, {
42
+ readonly id: "connect_your_tools";
43
+ readonly num: 4;
44
+ readonly name: "Connect Your Tools";
45
+ readonly tier: 1;
46
+ readonly tierName: "Orient & Equip";
47
+ readonly action: "Connect Slack and Gmail through claude.ai Integrations — type \"How do I connect my tools?\" for a walkthrough.";
48
+ readonly signal: "Connectors active — email and Slack flowing into your workspace";
49
+ }, {
50
+ readonly id: "morning_brief";
51
+ readonly num: 5;
52
+ readonly name: "Morning Brief";
53
+ readonly tier: 2;
54
+ readonly tierName: "The Operating System";
55
+ readonly action: "Start a session with \"What happened while I was away?\" or \"Morning brief\" — includes email triage, Slack synthesis, and calendar prep.";
56
+ readonly signal: "First daily operating picture with email triage, Slack synthesis, and calendar prep";
57
+ }, {
58
+ readonly id: "run_sales";
59
+ readonly num: 6;
60
+ readonly name: "Run Sales";
61
+ readonly tier: 2;
62
+ readonly tierName: "The Operating System";
63
+ readonly action: "Ask to prepare a proposal, run discovery on a prospect, or build a quote.";
64
+ readonly signal: "First compound business deliverable";
65
+ }, {
66
+ readonly id: "research_publish";
67
+ readonly num: 7;
68
+ readonly name: "Research & Publish";
69
+ readonly tier: 2;
70
+ readonly tierName: "The Operating System";
71
+ readonly action: "Ask to research a topic and publish the findings as institutional knowledge.";
72
+ readonly signal: "First published wiki article or KB entry";
73
+ }, {
74
+ readonly id: "audit_business";
75
+ readonly num: 8;
76
+ readonly name: "Audit Your Business";
77
+ readonly tier: 2;
78
+ readonly tierName: "The Operating System";
79
+ readonly action: "Ask \"How healthy is our business?\" or request a pulse audit.";
80
+ readonly signal: "First scored business health audit";
81
+ }, {
82
+ readonly id: "operating_rhythm";
83
+ readonly num: 9;
84
+ readonly name: "Your Operating Rhythm";
85
+ readonly tier: 3;
86
+ readonly tierName: "Agentic Mastery";
87
+ readonly action: "Set up weekly reviews, delegate recurring work to crews, and build your voice profile.";
88
+ readonly signal: "Return for second session with intent; establish a routine";
89
+ }];
3
90
  export declare function registerPrompts(server: McpServer, getClientFn?: () => Promise<TacitClient>): void;
package/dist/prompts.js CHANGED
@@ -45,7 +45,7 @@ function buildAssistantSystemPrompt(state) {
45
45
  lines.push('Prefer a single read tool for direct lookups, synthesis for open questions, and ask before any write or dispatch.');
46
46
  return lines.join('\n');
47
47
  }
48
- async function fetchWorkspaceState(client) {
48
+ export async function fetchWorkspaceState(client) {
49
49
  await client.initialize();
50
50
  const wsName = client.workspaceName || 'your workspace';
51
51
  const [collectionsRes, portfoliosRes, sourcesRes] = await Promise.all([
@@ -71,7 +71,7 @@ async function fetchWorkspaceState(client) {
71
71
  sourceCount: sources.length,
72
72
  };
73
73
  }
74
- const CAPABILITY_DOMAINS = [
74
+ export const CAPABILITY_DOMAINS = [
75
75
  { domain: 'Search & Recall', triggers: ['What do we know about…', 'Find me…', 'Look up…'], description: 'Search workspace knowledge and recall specific items' },
76
76
  { domain: 'Synthesis & Analysis', triggers: ['Summarize…', 'What does our data say about…', 'Compare…'], description: 'Grounded answers with citations from your workspace' },
77
77
  { domain: 'Knowledge Base', triggers: ['Create an article about…', 'Organize our knowledge on…'], description: 'Build and organize your reference library' },
@@ -91,7 +91,7 @@ const CAPABILITY_DOMAINS = [
91
91
  { domain: 'Connectors', triggers: ['Connect my Gmail…', 'Add Slack…'], description: 'Connect external tools via claude.ai Integrations' },
92
92
  { domain: 'Code & GitHub', triggers: ['Show my repo connections…', 'Sync from GitHub…'], description: 'Repository connections and code sync' },
93
93
  ];
94
- const EXPERIENCES = [
94
+ export const EXPERIENCES = [
95
95
  { id: 'first_light', num: 1, name: 'First Light', tier: 1, tierName: 'Orient & Equip', action: 'Ask a question about your business — try "What do we know about [topic]?"', signal: 'First grounded, cited answer about your company' },
96
96
  { id: 'feed_it', num: 2, name: 'Feed It', tier: 1, tierName: 'Orient & Equip', action: 'Add something to your workspace — share a URL, document, or file.', signal: 'First successful content ingestion' },
97
97
  { id: 'build_your_map', num: 3, name: 'Build Your Map', tier: 1, tierName: 'Orient & Equip', action: 'Organize your work — ask to create a collection, build a portfolio, or structure your business.', signal: 'First organizational write operation' },
package/dist/setup.js CHANGED
@@ -463,42 +463,60 @@ export async function setup(argv) {
463
463
  // fall back to env-var placeholder when the token came from the environment.
464
464
  const resolvedToken = token ?? null;
465
465
  const profileEntry = buildProfileEntry(apiUrl, envVarName, resolvedToken);
466
- const existing = readExistingConfig();
467
- const merged = mergeConfig(existing, profile, profileEntry);
468
- const output = configToBytes(merged);
469
466
  // D10: emit confirmation string BEFORE writing (also printed in dry-run).
470
- process.stdout.write(`Writing tacit entry to ~/.claude/mcp.json (profile: ${profile})...\n`);
467
+ process.stdout.write(`\n Registering MCP server "${profile}" with Claude Code...\n`);
471
468
  if (opts.dryRun) {
472
- // Redact any literal token in dry-run preview to avoid leaking secrets to stdout.
473
- const redacted = resolvedToken
474
- ? configToBytes(mergeConfig(existing, profile, buildProfileEntry(apiUrl, envVarName, null)))
475
- : output;
476
- process.stdout.write('(dry-run) Would write:\n');
477
- process.stdout.write(redacted);
469
+ process.stdout.write(' (dry-run) Would register:\n');
470
+ const redactedEntry = buildProfileEntry(apiUrl, envVarName, null);
471
+ process.stdout.write(` ${JSON.stringify(redactedEntry, null, 2)}\n`);
478
472
  process.exit(0);
479
473
  return;
480
474
  }
481
- // Create ~/.claude with mode 0700 if absent.
475
+ // Register via `claude mcp add-json` — the only way Claude Code discovers MCP servers.
476
+ // Writing ~/.claude/mcp.json directly does NOT work (Claude Code ignores that file).
477
+ const jsonConfig = JSON.stringify(profileEntry);
478
+ const registered = await new Promise((resolve) => {
479
+ execFile('claude', ['mcp', 'add-json', '--scope', 'user', profile, jsonConfig], {
480
+ timeout: 15_000,
481
+ }, (err, stdout, stderr) => {
482
+ if (err) {
483
+ process.stderr.write(`\n Warning: could not register via "claude mcp add-json".\n`);
484
+ process.stderr.write(` ${stderr || err.message}\n`);
485
+ resolve(false);
486
+ }
487
+ else {
488
+ resolve(true);
489
+ }
490
+ });
491
+ });
492
+ // Fallback: also write ~/.claude/mcp.json for environments without `claude` CLI.
482
493
  const dir = claudeDir();
483
494
  if (!fs.existsSync(dir)) {
484
495
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
485
496
  }
497
+ const existing = readExistingConfig();
498
+ const merged = mergeConfig(existing, profile, profileEntry);
499
+ const output = configToBytes(merged);
486
500
  const dest = mcpJsonPath();
487
501
  fs.writeFileSync(dest, output, { encoding: 'utf-8', mode: 0o600 });
488
- // Ensure 0600 even when the file pre-existed with looser permissions.
489
502
  fs.chmodSync(dest, 0o600);
490
503
  // AC-12: write ~/.tacit/profiles/<profile>.json metadata (token NOT included).
491
504
  if (expiresAt) {
492
505
  writeProfileMetadata(profile, expiresAt, apiUrl);
493
506
  }
494
- if (resolvedToken) {
495
- process.stdout.write(`\nSetup complete. Your token has been saved to ~/.claude/mcp.json.\n` +
496
- `Start a new Claude Code session to connect.\n`);
507
+ if (registered) {
508
+ process.stdout.write(`\n Done! Start a new Claude Code session to connect.\n`);
509
+ }
510
+ else if (resolvedToken) {
511
+ process.stdout.write(`\n Config written to ~/.claude/mcp.json (fallback).\n` +
512
+ ` If Claude Code doesn't pick it up, run:\n` +
513
+ ` claude mcp add-json --scope user ${profile} '${jsonConfig}'\n` +
514
+ ` Then start a new Claude Code session.\n`);
497
515
  }
498
516
  else {
499
- process.stdout.write(`\nSetup complete. Add this to your shell profile:\n` +
500
- ` export ${envVarName}=<your-token>\n` +
501
- `\nThen start a new Claude Code session to connect.\n`);
517
+ process.stdout.write(`\n Setup complete. Add this to your shell profile:\n` +
518
+ ` export ${envVarName}=<your-token>\n` +
519
+ `\n Then start a new Claude Code session to connect.\n`);
502
520
  }
503
521
  if (opts.smoke) {
504
522
  await runSmoke(apiUrl, token);
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TacitClient } from '../client.js';
3
+ export declare function registerOnboardingTools(server: McpServer, getClient: () => Promise<TacitClient>): void;
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod';
2
+ import { TacitApiError } from '../client.js';
3
+ import { success, error } from './errors.js';
4
+ import { EXPERIENCES, fetchWorkspaceState } from '../prompts.js';
5
+ export function registerOnboardingTools(server, getClient) {
6
+ server.tool('tacit_onboarding', 'Guide the user through the TACIT onboarding journey — the Nine Experiences that take them from first question to operating rhythm. ' +
7
+ 'Call this when the user says things like: "onboard me", "take me through onboarding", "show me the way", ' +
8
+ '"get me started", "what should I try next", "how do I get started", "walk me through setup", ' +
9
+ '"what are the experiences", "show me what I can do", or any request for guided onboarding.', {
10
+ completed: z
11
+ .string()
12
+ .optional()
13
+ .describe('Comma-separated experience IDs the user has already completed (e.g. "first_light,feed_it"). ' +
14
+ 'Omit if unknown — the tool will start from the beginning.'),
15
+ }, async (args) => {
16
+ try {
17
+ const client = await getClient();
18
+ const state = await fetchWorkspaceState(client);
19
+ const completedIds = (args.completed || '')
20
+ .split(',')
21
+ .map(s => s.trim().toLowerCase())
22
+ .filter(Boolean);
23
+ const completedSet = new Set(completedIds);
24
+ const tier1 = EXPERIENCES.filter(e => e.tier === 1);
25
+ const tier2 = EXPERIENCES.filter(e => e.tier === 2);
26
+ const tier3 = EXPERIENCES.filter(e => e.tier === 3);
27
+ const tier1Complete = tier1.every(e => completedSet.has(e.id));
28
+ let next;
29
+ if (!tier1Complete) {
30
+ next = tier1.find(e => !completedSet.has(e.id));
31
+ }
32
+ else {
33
+ next = tier2.find(e => !completedSet.has(e.id));
34
+ if (!next)
35
+ next = tier3.find(e => !completedSet.has(e.id));
36
+ }
37
+ const lines = [];
38
+ lines.push(`## Onboarding: ${state.name}`);
39
+ lines.push('');
40
+ lines.push(`Progress: ${completedIds.length} of ${EXPERIENCES.length} experiences complete.`);
41
+ lines.push('');
42
+ if (!next) {
43
+ lines.push('All nine experiences complete — you are a TACIT power user.');
44
+ lines.push('');
45
+ lines.push('From here, build your own operating rhythm: set up recurring briefs, delegate complex tasks to crews, and let the system learn your patterns.');
46
+ return success({ onboarding: lines.join('\n'), status: 'complete', completed: completedIds.length, total: EXPERIENCES.length });
47
+ }
48
+ lines.push(`### Next: Experience ${next.num} — ${next.name}`);
49
+ lines.push(`*${next.tierName}*`);
50
+ lines.push('');
51
+ lines.push(next.action);
52
+ lines.push('');
53
+ lines.push(`**When complete:** ${next.signal}`);
54
+ if (next.tier === 2 && tier1Complete) {
55
+ const otherTier2 = tier2.filter(e => e.id !== next.id && !completedSet.has(e.id));
56
+ if (otherTier2.length > 0) {
57
+ lines.push('');
58
+ lines.push(`Also available: ${otherTier2.map(e => e.name).join(', ')} — Tier 2 experiences work in any order.`);
59
+ }
60
+ }
61
+ lines.push('');
62
+ lines.push('### All Nine Experiences');
63
+ lines.push('');
64
+ for (const exp of EXPERIENCES) {
65
+ const done = completedSet.has(exp.id);
66
+ const current = exp.id === next.id;
67
+ const prefix = done ? '✓' : current ? '→' : '○';
68
+ lines.push(`${prefix} **${exp.num}. ${exp.name}** (${exp.tierName})${done ? ' — done' : current ? ' — up next' : ''}`);
69
+ }
70
+ return success({
71
+ onboarding: lines.join('\n'),
72
+ status: 'in_progress',
73
+ completed: completedIds.length,
74
+ total: EXPERIENCES.length,
75
+ next_experience: { id: next.id, num: next.num, name: next.name, tier: next.tier, action: next.action },
76
+ });
77
+ }
78
+ catch (e) {
79
+ if (e instanceof TacitApiError)
80
+ return error(e.message);
81
+ return error(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
82
+ }
83
+ });
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacitai/mcp-server",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "stdio MCP server wrapping Tacit /api/v1 REST/SSE API",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,