@tacitai/mcp-server 0.3.2 → 0.5.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/index.js CHANGED
@@ -48,6 +48,7 @@ import { registerPyramidTools } from './tools/pyramid.js';
48
48
  import { registerSharingTools } from './tools/sharing.js';
49
49
  import { registerKbTools } from './tools/kb.js';
50
50
  import { registerLakeTools } from './tools/lake.js';
51
+ import { registerGithubTools } from './tools/github.js';
51
52
  import { registerWebUrlTools } from './tools/web-url.js';
52
53
  import { registerWorkspaceTools } from './tools/workspaces.js';
53
54
  import { registerChronicleTools } from './tools/chronicle.js';
@@ -57,6 +58,8 @@ import { registerLakeStructureTools } from './tools/lake-structure.js';
57
58
  import { registerResearchTools } from './tools/research.js';
58
59
  import { registerGmTools } from './tools/gm.js';
59
60
  import { registerSkillTools } from './tools/skills.js';
61
+ import { registerWikiTools } from './tools/wiki.js';
62
+ import { registerVoiceTools } from './tools/voice.js';
60
63
  import { registerPrompts } from './prompts.js';
61
64
  import { getRequestPrincipal } from './client.js';
62
65
  // Setup wizard (MV-1057h / W5)
@@ -169,6 +172,7 @@ async function run() {
169
172
  registerSharingTools(server, getClient);
170
173
  registerKbTools(server, getClient);
171
174
  registerLakeTools(server, getClient);
175
+ registerGithubTools(server, getClient);
172
176
  registerWebUrlTools(server);
173
177
  registerWorkspaceTools(server, getClient);
174
178
  registerChronicleTools(server, getClient);
@@ -178,7 +182,9 @@ async function run() {
178
182
  registerGmTools(server, getClient);
179
183
  registerResearchTools(server, getClient, getRequestPrincipal);
180
184
  registerSkillTools(server, getClient);
181
- registerPrompts(server);
185
+ registerWikiTools(server, getClient);
186
+ registerVoiceTools(server, getClient);
187
+ registerPrompts(server, getClient);
182
188
  if (process.env['MCP_TRANSPORT'] === 'http') {
183
189
  await startHttpTransport(server);
184
190
  return;
package/dist/prompts.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function registerPrompts(server: McpServer): void;
2
+ import type { TacitClient } from './client.js';
3
+ export declare function registerPrompts(server: McpServer, getClientFn?: () => Promise<TacitClient>): void;
package/dist/prompts.js CHANGED
@@ -1,12 +1,294 @@
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
- export function registerPrompts(server) {
9
- server.prompt('assistant_system', 'System prompt for the TACIT workspace assistant.', () => userText(ASSISTANT_SYSTEM_PROMPT));
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
+ }
49
+ async function fetchWorkspaceState(client) {
50
+ await client.initialize();
51
+ const wsName = client.workspaceName || 'your workspace';
52
+ const [collectionsRes, portfoliosRes, sourcesRes] = await Promise.all([
53
+ client.get('/api/v1/knowledge/collections').catch(() => []),
54
+ client.get('/api/v1/portfolios').catch(() => []),
55
+ client.get('/api/v1/org/lake/sources').catch(() => []),
56
+ ]);
57
+ const collections = Array.isArray(collectionsRes)
58
+ ? collectionsRes
59
+ : collectionsRes?.collections ?? [];
60
+ const portfolios = Array.isArray(portfoliosRes)
61
+ ? portfoliosRes
62
+ : portfoliosRes?.portfolios ?? [];
63
+ const sources = Array.isArray(sourcesRes)
64
+ ? sourcesRes
65
+ : sourcesRes?.sources ?? [];
66
+ return {
67
+ name: wsName,
68
+ collectionNames: collections.map(c => c.name || c.display_name || '').filter(Boolean).slice(0, 5),
69
+ collectionCount: collections.length,
70
+ portfolioNames: portfolios.map(p => p.name || p.display_name || '').filter(Boolean).slice(0, 5),
71
+ portfolioCount: portfolios.length,
72
+ sourceCount: sources.length,
73
+ };
74
+ }
75
+ const CAPABILITY_DOMAINS = [
76
+ { domain: 'Search & Recall', triggers: ['What do we know about…', 'Find me…', 'Look up…'], description: 'Search workspace knowledge and recall specific items' },
77
+ { domain: 'Synthesis & Analysis', triggers: ['Summarize…', 'What does our data say about…', 'Compare…'], description: 'Grounded answers with citations from your workspace' },
78
+ { domain: 'Knowledge Base', triggers: ['Create an article about…', 'Organize our knowledge on…'], description: 'Build and organize your reference library' },
79
+ { domain: 'Ingestion', triggers: ['Bring in this URL…', 'Add this document…'], description: 'Add content from URLs, files, and repositories' },
80
+ { domain: 'Business Structure', triggers: ['Show me our portfolios…', 'Create a program for…'], description: 'Organize work into portfolios, programs, and dossiers' },
81
+ { domain: 'Data Lake', triggers: ['What is in our data lake?', 'Classify this…'], description: 'Raw data storage, classification, and source management' },
82
+ { domain: 'Voice & Composition', triggers: ['Learn how I write…', 'Draft this in my voice…'], description: 'Learn your writing style and compose in your voice' },
83
+ { domain: 'Timeline & Chronicle', triggers: ['What happened last week?', 'Show me the timeline…'], description: 'Event timeline and organizational memory' },
84
+ { domain: 'Agents & Crews', triggers: ['Can someone else do this?', 'Delegate this…'], description: 'Dispatch autonomous agents for complex deliverables' },
85
+ { domain: 'Documents & Artifacts', triggers: ['Create a report…', 'Draft a document…'], description: 'Create, version, and share structured documents' },
86
+ { domain: 'Sharing & Outbox', triggers: ['Share this with…', 'Send this to…'], description: 'Queue and send work products externally' },
87
+ { domain: 'Research', triggers: ['Research this topic…', 'What is the latest on…'], description: 'Deep research with source verification' },
88
+ { domain: 'Wiki Authoring', triggers: ['Write a wiki article…', 'Validate this for publishing…'], description: 'Author, validate, and publish institutional knowledge' },
89
+ { domain: 'People & Contacts', triggers: ['Who is…', 'Add a contact for…'], description: 'Manage contacts and organizational relationships' },
90
+ { domain: 'Daily Operations', triggers: ['Morning brief', 'Triage my email…', 'Prep for my meeting…'], description: 'Daily briefs, email triage, meeting prep, and operational reviews' },
91
+ { domain: 'Sales', triggers: ['Prepare a proposal for…', 'Run discovery on…'], description: 'End-to-end sales workflows from context brief to pipeline' },
92
+ { domain: 'Connectors', triggers: ['Connect my Gmail…', 'Add Slack…'], description: 'Connect external tools via claude.ai Integrations' },
93
+ { domain: 'Code & GitHub', triggers: ['Show my repo connections…', 'Sync from GitHub…'], description: 'Repository connections and code sync' },
94
+ ];
95
+ const EXPERIENCES = [
96
+ { 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' },
97
+ { 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' },
98
+ { 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' },
99
+ { id: 'connect_your_tools', num: 4, name: 'Connect Your Tools', tier: 1, tierName: 'Orient & Equip', action: 'Connect Slack and Gmail through claude.ai Integrations — type "How do I connect my tools?" for a walkthrough.', signal: 'Connectors active — email and Slack flowing into your workspace' },
100
+ { id: 'morning_brief', num: 5, name: 'Morning Brief', tier: 2, tierName: 'The Operating System', action: 'Start a session with "What happened while I was away?" or "Morning brief" — includes email triage, Slack synthesis, and calendar prep.', signal: 'First daily operating picture with email triage, Slack synthesis, and calendar prep' },
101
+ { id: 'run_sales', num: 6, name: 'Run Sales', tier: 2, tierName: 'The Operating System', action: 'Ask to prepare a proposal, run discovery on a prospect, or build a quote.', signal: 'First compound business deliverable' },
102
+ { id: 'research_publish', num: 7, name: 'Research & Publish', tier: 2, tierName: 'The Operating System', action: 'Ask to research a topic and publish the findings as institutional knowledge.', signal: 'First published wiki article or KB entry' },
103
+ { id: 'audit_business', num: 8, name: 'Audit Your Business', tier: 2, tierName: 'The Operating System', action: 'Ask "How healthy is our business?" or request a pulse audit.', signal: 'First scored business health audit' },
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' },
105
+ ];
106
+ export function registerPrompts(server, getClientFn) {
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
+ });
10
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}`));
11
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}`));
122
+ if (!getClientFn)
123
+ return;
124
+ // --- welcome: personalized workspace greeting with state summary ---
125
+ server.prompt('welcome', 'Personalized workspace welcome with state summary and suggested first questions.', { workspace_name: z.string().optional().describe('Override workspace display name') }, async (args) => {
126
+ const client = await getClientFn();
127
+ const state = await fetchWorkspaceState(client);
128
+ const name = args.workspace_name || state.name;
129
+ const lines = [];
130
+ lines.push(`Welcome to ${name}.`);
131
+ lines.push('');
132
+ const counts = [];
133
+ if (state.collectionCount > 0)
134
+ counts.push(`${state.collectionCount} knowledge collection${state.collectionCount === 1 ? '' : 's'}`);
135
+ if (state.portfolioCount > 0)
136
+ counts.push(`${state.portfolioCount} portfolio${state.portfolioCount === 1 ? '' : 's'}`);
137
+ if (state.sourceCount > 0)
138
+ counts.push(`${state.sourceCount} connected source${state.sourceCount === 1 ? '' : 's'}`);
139
+ if (counts.length > 0) {
140
+ lines.push(`Your workspace has ${counts.join(', ')}.`);
141
+ }
142
+ else {
143
+ lines.push('Your workspace is fresh — ready for its first content.');
144
+ }
145
+ lines.push('');
146
+ lines.push('Here are some questions to start with:');
147
+ let qNum = 1;
148
+ if (state.collectionNames.length > 0) {
149
+ lines.push(` ${qNum++}. "What do we know about ${state.collectionNames[0]}?"`);
150
+ if (state.collectionNames.length > 1) {
151
+ lines.push(` ${qNum++}. "Summarize our knowledge on ${state.collectionNames[1]}"`);
152
+ }
153
+ }
154
+ if (state.portfolioNames.length > 0) {
155
+ lines.push(` ${qNum++}. "Show me the ${state.portfolioNames[0]} portfolio"`);
156
+ }
157
+ if (qNum === 1) {
158
+ lines.push(' 1. "What can you help me with?"');
159
+ lines.push(' 2. "Help me add my first document"');
160
+ lines.push(' 3. "Show me how to organize my business"');
161
+ }
162
+ lines.push('');
163
+ lines.push('Ask anything about your business — every answer is grounded in your workspace knowledge with cited sources.');
164
+ return userText(lines.join('\n'));
165
+ });
166
+ // --- first_question: content-driven question suggestions ---
167
+ server.prompt('first_question', 'Content-driven question suggestions drawn from actual workspace data.', {}, async () => {
168
+ const client = await getClientFn();
169
+ const state = await fetchWorkspaceState(client);
170
+ const suggestions = [];
171
+ for (const name of state.collectionNames.slice(0, 3)) {
172
+ suggestions.push(`"What do we know about ${name}?"`);
173
+ }
174
+ for (const name of state.portfolioNames.slice(0, 2)) {
175
+ suggestions.push(`"What is the status of ${name}?"`);
176
+ }
177
+ if (suggestions.length === 0) {
178
+ suggestions.push('"What can you help me with?"');
179
+ suggestions.push('"Help me bring in my first document"');
180
+ suggestions.push('"Show me how to organize my business"');
181
+ }
182
+ const lines = [
183
+ `Based on what is in ${state.name}, here are questions worth asking:`,
184
+ '',
185
+ ...suggestions.map((s, i) => ` ${i + 1}. ${s}`),
186
+ '',
187
+ 'Pick one, or ask your own — every answer is grounded in your workspace data.',
188
+ ];
189
+ return userText(lines.join('\n'));
190
+ });
191
+ // --- explore: NL capability browser, zero tool names ---
192
+ server.prompt('explore', 'Browse what your workspace can do, organized by what you want to accomplish.', { domain: z.string().optional().describe('Filter to a specific domain, e.g. "sales" or "knowledge"') }, async (args) => {
193
+ const filter = args.domain?.toLowerCase();
194
+ const domains = filter
195
+ ? CAPABILITY_DOMAINS.filter(d => d.domain.toLowerCase().includes(filter) ||
196
+ d.description.toLowerCase().includes(filter))
197
+ : CAPABILITY_DOMAINS;
198
+ if (domains.length === 0) {
199
+ return userText(`No capabilities match "${args.domain}". Available domains: ${CAPABILITY_DOMAINS.map(d => d.domain).join(', ')}`);
200
+ }
201
+ const lines = [
202
+ filter ? `Capabilities matching "${args.domain}":` : 'Everything your workspace can do:',
203
+ '',
204
+ ];
205
+ for (const d of domains) {
206
+ lines.push(`**${d.domain}** — ${d.description}`);
207
+ lines.push(` Try: ${d.triggers.join(' · ')}`);
208
+ lines.push('');
209
+ }
210
+ lines.push(`${CAPABILITY_DOMAINS.length} capability domains. Describe what you need in your own words.`);
211
+ return userText(lines.join('\n'));
212
+ });
213
+ // --- whats_next: progressive experience suggestion ---
214
+ server.prompt('whats_next', 'Suggest the next onboarding experience based on what you have already completed.', {
215
+ completed: z.string().optional().describe('Comma-separated experience IDs already completed (e.g. "first_light,feed_it")'),
216
+ }, async (args) => {
217
+ const completedIds = (args.completed || '')
218
+ .split(',')
219
+ .map(s => s.trim().toLowerCase())
220
+ .filter(Boolean);
221
+ const completedSet = new Set(completedIds);
222
+ const tier1 = EXPERIENCES.filter(e => e.tier === 1);
223
+ const tier2 = EXPERIENCES.filter(e => e.tier === 2);
224
+ const tier3 = EXPERIENCES.filter(e => e.tier === 3);
225
+ const tier1Complete = tier1.every(e => completedSet.has(e.id));
226
+ let next;
227
+ if (!tier1Complete) {
228
+ next = tier1.find(e => !completedSet.has(e.id));
229
+ }
230
+ else {
231
+ next = tier2.find(e => !completedSet.has(e.id));
232
+ if (!next)
233
+ next = tier3.find(e => !completedSet.has(e.id));
234
+ }
235
+ if (!next) {
236
+ return userText('You have completed all nine experiences — you are a TACIT power user.\n\n' +
237
+ 'From here, build your own operating rhythm: set up recurring briefs, delegate complex tasks to crews, ' +
238
+ 'and let the system learn your patterns. Ask "What can I do next?" any time.');
239
+ }
240
+ const lines = [
241
+ `Progress: ${completedIds.length} of 9 experiences complete.`,
242
+ '',
243
+ `**Next: Experience ${next.num} — ${next.name}** (${next.tierName})`,
244
+ '',
245
+ next.action,
246
+ '',
247
+ `When complete: ${next.signal}`,
248
+ ];
249
+ if (next.tier === 2 && tier1Complete) {
250
+ const otherTier2 = tier2.filter(e => e.id !== next.id && !completedSet.has(e.id));
251
+ if (otherTier2.length > 0) {
252
+ lines.push('');
253
+ lines.push(`Also available: ${otherTier2.map(e => e.name).join(', ')} — Tier 2 experiences work in any order.`);
254
+ }
255
+ }
256
+ return userText(lines.join('\n'));
257
+ });
258
+ // --- setup_guide: Slack + Gmail connector walkthrough ---
259
+ server.prompt('setup_guide', 'Step-by-step guide to connect Slack and Gmail through claude.ai Integrations.', { connector: z.string().optional().describe('Which connector: "slack", "gmail", or "both" (default)') }, async (args) => {
260
+ const which = (args.connector || 'both').toLowerCase();
261
+ const showSlack = which === 'slack' || which === 'both';
262
+ const showGmail = which === 'gmail' || which === 'both';
263
+ const lines = [];
264
+ lines.push('Connect your tools to give TACIT access to your communications.');
265
+ lines.push('This is done through claude.ai — not the CLI.');
266
+ lines.push('');
267
+ if (showSlack) {
268
+ lines.push('**Connect Slack**');
269
+ lines.push(' 1. Open claude.ai in your browser');
270
+ lines.push(' 2. Go to Settings → Integrations (or click your profile → Integrations)');
271
+ lines.push(' 3. Find "Slack" and click Connect');
272
+ lines.push(' 4. Authorize access to your Slack workspace');
273
+ lines.push(' 5. Select which channels to make available');
274
+ lines.push(' 6. Return to Claude Code — Slack data now flows into your sessions');
275
+ lines.push('');
276
+ lines.push(' Once connected, your Morning Brief includes overnight Slack activity.');
277
+ lines.push('');
278
+ }
279
+ if (showGmail) {
280
+ lines.push('**Connect Gmail**');
281
+ lines.push(' 1. Open claude.ai in your browser');
282
+ lines.push(' 2. Go to Settings → Integrations');
283
+ lines.push(' 3. Find "Gmail" and click Connect');
284
+ lines.push(' 4. Sign in with your Google account and grant access');
285
+ lines.push(' 5. Return to Claude Code — email data now flows into your sessions');
286
+ lines.push('');
287
+ lines.push(' Once connected, your Morning Brief includes email triage using the 4D method:');
288
+ lines.push(' Delete (discard), Do (act now), Delegate (forward), Defer (schedule).');
289
+ lines.push('');
290
+ }
291
+ lines.push('After connecting, try Experience 5 — Morning Brief — to see everything synthesized into a daily operating picture.');
292
+ return userText(lines.join('\n'));
293
+ });
12
294
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * GitHub connection tools — list, disconnect and reconnect GitHub sync
3
+ * connections (BUG-305).
4
+ *
5
+ * Routes (Axum, all under /api/v1/integrations/github):
6
+ * GET /connections[?connection_id=<uuid>]
7
+ * POST /:connection_id/disconnect
8
+ * POST /:connection_id/reconnect
9
+ */
10
+ import { z } from 'zod';
11
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { TacitClient } from '../client.js';
13
+ export interface ToolRequest {
14
+ execute(): Promise<{
15
+ content: Array<{
16
+ type: 'text';
17
+ text: string;
18
+ }>;
19
+ isError?: boolean;
20
+ }>;
21
+ }
22
+ export declare const listConnectionsInputSchema: z.ZodObject<{
23
+ connection_id: z.ZodOptional<z.ZodString>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ connection_id?: string | undefined;
26
+ }, {
27
+ connection_id?: string | undefined;
28
+ }>;
29
+ export type ListConnectionsInput = z.infer<typeof listConnectionsInputSchema>;
30
+ export declare const LIST_CONNECTIONS_TOOL_DEF: {
31
+ name: "tacit_github_list_connections";
32
+ description: string;
33
+ };
34
+ export declare function buildListConnectionsRequest(client: TacitClient, input: ListConnectionsInput): ToolRequest;
35
+ export declare const disconnectConnectionInputSchema: z.ZodObject<{
36
+ connection_id: z.ZodString;
37
+ }, "strip", z.ZodTypeAny, {
38
+ connection_id: string;
39
+ }, {
40
+ connection_id: string;
41
+ }>;
42
+ export type DisconnectConnectionInput = z.infer<typeof disconnectConnectionInputSchema>;
43
+ export declare const DISCONNECT_CONNECTION_TOOL_DEF: {
44
+ name: "tacit_github_disconnect_connection";
45
+ description: string;
46
+ };
47
+ export declare function buildDisconnectConnectionRequest(client: TacitClient, input: DisconnectConnectionInput): ToolRequest;
48
+ export declare const reconnectConnectionInputSchema: z.ZodObject<{
49
+ connection_id: z.ZodString;
50
+ }, "strip", z.ZodTypeAny, {
51
+ connection_id: string;
52
+ }, {
53
+ connection_id: string;
54
+ }>;
55
+ export type ReconnectConnectionInput = z.infer<typeof reconnectConnectionInputSchema>;
56
+ export declare const RECONNECT_CONNECTION_TOOL_DEF: {
57
+ name: "tacit_github_reconnect_connection";
58
+ description: string;
59
+ };
60
+ export declare function buildReconnectConnectionRequest(client: TacitClient, input: ReconnectConnectionInput): ToolRequest;
61
+ export declare function registerGithubTools(server: McpServer, getClientFn: () => Promise<TacitClient>): void;
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * GitHub connection tools — list, disconnect and reconnect GitHub sync
4
+ * connections (BUG-305).
5
+ *
6
+ * Routes (Axum, all under /api/v1/integrations/github):
7
+ * GET /connections[?connection_id=<uuid>]
8
+ * POST /:connection_id/disconnect
9
+ * POST /:connection_id/reconnect
10
+ */
11
+ import { z } from 'zod';
12
+ import { TacitApiError, TokenExpiredError } from '../client.js';
13
+ function apiErrorResult(err, toolName) {
14
+ if (err instanceof TokenExpiredError) {
15
+ return { content: [{ type: 'text', text: err.message }], isError: true };
16
+ }
17
+ if (err instanceof TacitApiError) {
18
+ return { content: [{ type: 'text', text: `API error ${err.statusCode} on ${toolName}: ${err.body}` }], isError: true };
19
+ }
20
+ const msg = err instanceof Error ? err.message : String(err);
21
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
22
+ }
23
+ // ─── tacit_github_list_connections ──────────────────────────────────────────
24
+ export const listConnectionsInputSchema = z.object({
25
+ connection_id: z.string().uuid().optional().describe('Return only this connection'),
26
+ });
27
+ export const LIST_CONNECTIONS_TOOL_DEF = {
28
+ name: 'tacit_github_list_connections',
29
+ description: 'List GitHub sync connections for the current workspace with status, last_polled_at, last_synced_at, disconnected_at/by, file_link_count and max_link_last_synced_at. Route: GET /api/v1/integrations/github/connections.',
30
+ };
31
+ export function buildListConnectionsRequest(client, input) {
32
+ return {
33
+ async execute() {
34
+ try {
35
+ const result = await client.get('/api/v1/integrations/github/connections', input.connection_id ? { connection_id: input.connection_id } : undefined);
36
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
37
+ }
38
+ catch (err) {
39
+ return apiErrorResult(err, 'tacit_github_list_connections');
40
+ }
41
+ },
42
+ };
43
+ }
44
+ // ─── tacit_github_disconnect_connection ─────────────────────────────────────
45
+ export const disconnectConnectionInputSchema = z.object({
46
+ connection_id: z.string().uuid().describe('GitHub connection to disconnect'),
47
+ });
48
+ export const DISCONNECT_CONNECTION_TOOL_DEF = {
49
+ name: 'tacit_github_disconnect_connection',
50
+ description: 'Disconnect a GitHub sync connection. APPROVAL-TIER: confirm with the user before calling. Stops polling and sync; existing history and file links are kept. Requires auth.manage. Route: POST /api/v1/integrations/github/{connection_id}/disconnect.',
51
+ };
52
+ export function buildDisconnectConnectionRequest(client, input) {
53
+ return {
54
+ async execute() {
55
+ try {
56
+ const result = await client.post(`/api/v1/integrations/github/${input.connection_id}/disconnect`, {});
57
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
58
+ }
59
+ catch (err) {
60
+ return apiErrorResult(err, 'tacit_github_disconnect_connection');
61
+ }
62
+ },
63
+ };
64
+ }
65
+ // ─── tacit_github_reconnect_connection ──────────────────────────────────────
66
+ export const reconnectConnectionInputSchema = z.object({
67
+ connection_id: z.string().uuid().describe('GitHub connection to reconnect'),
68
+ });
69
+ export const RECONNECT_CONNECTION_TOOL_DEF = {
70
+ name: 'tacit_github_reconnect_connection',
71
+ description: 'Reconnect a previously disconnected GitHub sync connection. APPROVAL-TIER: confirm with the user before calling. Resumes polling and sync. Requires auth.manage. Route: POST /api/v1/integrations/github/{connection_id}/reconnect.',
72
+ };
73
+ export function buildReconnectConnectionRequest(client, input) {
74
+ return {
75
+ async execute() {
76
+ try {
77
+ const result = await client.post(`/api/v1/integrations/github/${input.connection_id}/reconnect`, {});
78
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
79
+ }
80
+ catch (err) {
81
+ return apiErrorResult(err, 'tacit_github_reconnect_connection');
82
+ }
83
+ },
84
+ };
85
+ }
86
+ // ─── Registration ───────────────────────────────────────────────────────────
87
+ export function registerGithubTools(server, getClientFn) {
88
+ server.tool('tacit_github_list_connections', LIST_CONNECTIONS_TOOL_DEF.description, listConnectionsInputSchema.shape, async (args) => {
89
+ const client = await getClientFn();
90
+ return buildListConnectionsRequest(client, args).execute();
91
+ });
92
+ server.tool('tacit_github_disconnect_connection', DISCONNECT_CONNECTION_TOOL_DEF.description, disconnectConnectionInputSchema.shape, async (args) => {
93
+ const client = await getClientFn();
94
+ return buildDisconnectConnectionRequest(client, args).execute();
95
+ });
96
+ server.tool('tacit_github_reconnect_connection', RECONNECT_CONNECTION_TOOL_DEF.description, reconnectConnectionInputSchema.shape, async (args) => {
97
+ const client = await getClientFn();
98
+ return buildReconnectConnectionRequest(client, args).execute();
99
+ });
100
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Compile-time tool classification. Governs which tools may be called over HTTPS.
3
- * Partition: 6 stdio_only + 15 admin_only + 2 gate_only + 110 https = 133.
3
+ * Partition: 6 stdio_only + 17 admin_only + 2 gate_only + 121 https = 146.
4
4
  */
5
5
  export type ToolTransport = 'https' | 'stdio_only' | 'admin_only' | 'gate_only';
6
6
  export type ToolClass = 'read' | 'write' | 'admin' | 'dispatch';
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  /**
3
3
  * Compile-time tool classification. Governs which tools may be called over HTTPS.
4
- * Partition: 6 stdio_only + 15 admin_only + 2 gate_only + 110 https = 133.
4
+ * Partition: 6 stdio_only + 17 admin_only + 2 gate_only + 121 https = 146.
5
5
  */
6
6
  const STDIO_ONLY = [
7
7
  'tacit_ingest_file',
@@ -25,6 +25,8 @@ const ADMIN_ONLY = [
25
25
  'tacit_lake_folder_merge',
26
26
  'tacit_lake_folder_remove',
27
27
  'tacit_lake_delete_catalog_item',
28
+ 'tacit_github_disconnect_connection',
29
+ 'tacit_github_reconnect_connection',
28
30
  'tacit_compose',
29
31
  'tacit_converse',
30
32
  ];
@@ -66,6 +68,7 @@ const READ_TOOLS = [
66
68
  'tacit_lake_folder_list',
67
69
  'tacit_lake_list_proposals',
68
70
  'tacit_lake_list_sources',
71
+ 'tacit_github_list_connections',
69
72
  'tacit_lake_manifest',
70
73
  'tacit_lake_resolve',
71
74
  'tacit_lake_sync_status',
@@ -90,10 +93,16 @@ const READ_TOOLS = [
90
93
  'tacit_shared_read',
91
94
  'tacit_shared_search',
92
95
  'tacit_synthesize',
96
+ 'tacit_voice_get_profile',
97
+ 'tacit_voice_get_stats',
98
+ 'tacit_voice_list_profiles',
93
99
  'tacit_web_url',
100
+ 'tacit_wiki_classify',
101
+ 'tacit_wiki_validate',
94
102
  ];
95
103
  const DISPATCH_TOOLS = [
96
104
  'tacit_agent_delegate',
105
+ 'tacit_compose_in_voice',
97
106
  'tacit_dispatch_crew',
98
107
  'tacit_kb_pull_source',
99
108
  'tacit_lake_trigger_sync',
@@ -152,6 +161,10 @@ const WRITE_TOOLS = [
152
161
  'tacit_research_prepare',
153
162
  'tacit_share_artifact',
154
163
  'tacit_switch_workspace',
164
+ 'tacit_voice_upload_sample',
165
+ 'tacit_wiki_author',
166
+ 'tacit_wiki_override',
167
+ 'tacit_wiki_write',
155
168
  ];
156
169
  function build() {
157
170
  const map = new Map();
@@ -178,8 +191,8 @@ function build() {
178
191
  if (!exclusive.has(n))
179
192
  add(n, classOf(n), 'https', false);
180
193
  }
181
- if (map.size !== 133)
182
- throw new Error(`tool registry expected 133 tools, got ${map.size}`);
194
+ if (map.size !== 146)
195
+ throw new Error(`tool registry expected 146 tools, got ${map.size}`);
183
196
  return map;
184
197
  }
185
198
  export const TOOL_REGISTRY = build();
@@ -20,6 +20,8 @@ export declare const synthesizeInputSchema: z.ZodObject<{
20
20
  origin: z.ZodOptional<z.ZodString>;
21
21
  synthesis_mode: z.ZodOptional<z.ZodEnum<["burst", "orbit", "crew_compose"]>>;
22
22
  max_turns: z.ZodOptional<z.ZodNumber>;
23
+ voice_mode: z.ZodOptional<z.ZodEnum<["voice_on", "voice_off", "voice_only"]>>;
24
+ voice_profile_id: z.ZodOptional<z.ZodString>;
23
25
  }, "strip", z.ZodTypeAny, {
24
26
  query: string;
25
27
  workspace_id?: string | undefined;
@@ -30,6 +32,8 @@ export declare const synthesizeInputSchema: z.ZodObject<{
30
32
  program_ids?: string[] | undefined;
31
33
  synthesis_mode?: "burst" | "orbit" | "crew_compose" | undefined;
32
34
  max_turns?: number | undefined;
35
+ voice_mode?: "voice_on" | "voice_off" | "voice_only" | undefined;
36
+ voice_profile_id?: string | undefined;
33
37
  }, {
34
38
  query: string;
35
39
  workspace_id?: string | undefined;
@@ -40,6 +44,8 @@ export declare const synthesizeInputSchema: z.ZodObject<{
40
44
  program_ids?: string[] | undefined;
41
45
  synthesis_mode?: "burst" | "orbit" | "crew_compose" | undefined;
42
46
  max_turns?: number | undefined;
47
+ voice_mode?: "voice_on" | "voice_off" | "voice_only" | undefined;
48
+ voice_profile_id?: string | undefined;
43
49
  }>;
44
50
  export type SynthesizeInput = z.infer<typeof synthesizeInputSchema>;
45
51
  export declare const SYNTHESIZE_TOOL_DEF: {
@@ -92,6 +98,15 @@ export declare const SYNTHESIZE_TOOL_DEF: {
92
98
  minimum: number;
93
99
  description: string;
94
100
  };
101
+ voice_mode: {
102
+ type: string;
103
+ enum: string[];
104
+ description: string;
105
+ };
106
+ voice_profile_id: {
107
+ type: string;
108
+ description: string;
109
+ };
95
110
  };
96
111
  required: string[];
97
112
  };
@@ -20,6 +20,10 @@ export const synthesizeInputSchema = z.object({
20
20
  origin: z.string().optional().describe('Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results'),
21
21
  synthesis_mode: z.enum(['burst', 'orbit', 'crew_compose']).optional().describe('Synthesis mode: burst (fast retrieval), orbit (agentic multi-turn), crew_compose (crew artifact generation). Default: burst.'),
22
22
  max_turns: z.number().int().min(1).optional().describe('Max agentic loop iterations for orbit/crew_compose mode (1-12 for orbit, 1-54 for crew_compose).'),
23
+ voice_mode: z.enum(['voice_on', 'voice_off', 'voice_only']).optional()
24
+ .describe('Voice personalization mode. voice_on: apply active profile, voice_off: suppress, voice_only: voice without synthesis.'),
25
+ voice_profile_id: z.string().optional()
26
+ .describe('Specific voice profile UUID. Omit to use the active profile when voice_mode is set.'),
23
27
  });
24
28
  export const SYNTHESIZE_TOOL_DEF = {
25
29
  name: 'tacit_synthesize',
@@ -38,6 +42,8 @@ export const SYNTHESIZE_TOOL_DEF = {
38
42
  origin: { type: 'string', description: 'Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results' },
39
43
  synthesis_mode: { type: 'string', enum: ['burst', 'orbit', 'crew_compose'], description: 'Synthesis mode: burst (fast retrieval), orbit (agentic multi-turn), crew_compose (crew artifact generation). Default: burst.' },
40
44
  max_turns: { type: 'integer', minimum: 1, description: 'Max agentic loop iterations for orbit/crew_compose mode (1-12 for orbit, 1-54 for crew_compose).' },
45
+ voice_mode: { type: 'string', enum: ['voice_on', 'voice_off', 'voice_only'], description: 'Voice personalization mode. Supports optional voice_mode for voice-personalized output.' },
46
+ voice_profile_id: { type: 'string', description: 'Specific voice profile UUID. Omit to use the active profile when voice_mode is set.' },
41
47
  },
42
48
  required: ['query'],
43
49
  },
@@ -49,7 +55,7 @@ export const SYNTHESIZE_TOOL_DEF = {
49
55
  export function buildSynthesizeRequest(client, input) {
50
56
  return {
51
57
  async execute() {
52
- const { query, scope, stream, workspace_id, collection_ids, program_ids, origin, synthesis_mode, max_turns } = input;
58
+ const { query, scope, stream, workspace_id, collection_ids, program_ids, origin, synthesis_mode, max_turns, voice_mode, voice_profile_id } = input;
53
59
  const body = { query };
54
60
  if (scope)
55
61
  body['scope'] = scope;
@@ -65,6 +71,10 @@ export function buildSynthesizeRequest(client, input) {
65
71
  body['synthesis_mode'] = synthesis_mode;
66
72
  if (max_turns)
67
73
  body['max_turns'] = max_turns;
74
+ if (voice_mode)
75
+ body['voice_mode'] = voice_mode;
76
+ if (voice_profile_id)
77
+ body['voice_profile_id'] = voice_profile_id;
68
78
  try {
69
79
  if (stream) {
70
80
  // SSE path: POST /api/v1/mind/synthesize/stream
@@ -138,6 +148,10 @@ const SYNTH_SCHEMA = {
138
148
  origin: z.string().optional().describe('Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results'),
139
149
  synthesis_mode: z.enum(['burst', 'orbit', 'crew_compose']).optional().describe('Synthesis mode: burst (fast retrieval), orbit (agentic multi-turn), crew_compose (crew artifact generation). Default: burst.'),
140
150
  max_turns: z.number().int().min(1).optional().describe('Max agentic loop iterations for orbit/crew_compose mode (1-12 for orbit, 1-54 for crew_compose).'),
151
+ voice_mode: z.enum(['voice_on', 'voice_off', 'voice_only']).optional()
152
+ .describe('Voice personalization mode. voice_on: apply active profile, voice_off: suppress, voice_only: voice without synthesis.'),
153
+ voice_profile_id: z.string().optional()
154
+ .describe('Specific voice profile UUID. Omit to use the active profile when voice_mode is set.'),
141
155
  };
142
156
  export function registerSynthesizeTools(server, getClientFn) {
143
157
  const handler = async (args) => {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Voice resonance tools — profile management, stats, sample upload,
3
+ * and voice-applied composition via crew_compose + voice_mode.
4
+ *
5
+ * Routes (Axum, under /api/v1/voice):
6
+ * GET /profiles — list voice profiles
7
+ * GET /profiles/:id — get profile detail
8
+ * GET /profiles/:id/stats — get profile statistics
9
+ * POST /profiles/:id/samples — upload a voice sample
10
+ *
11
+ * compose_in_voice uses POST /api/v1/mind/synthesize with
12
+ * synthesis_mode=crew_compose + voice_mode=voice_on.
13
+ */
14
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { TacitClient } from '../client.js';
16
+ export declare function registerVoiceTools(server: McpServer, getClientFn: () => Promise<TacitClient>): void;
@@ -0,0 +1,101 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Voice resonance tools — profile management, stats, sample upload,
4
+ * and voice-applied composition via crew_compose + voice_mode.
5
+ *
6
+ * Routes (Axum, under /api/v1/voice):
7
+ * GET /profiles — list voice profiles
8
+ * GET /profiles/:id — get profile detail
9
+ * GET /profiles/:id/stats — get profile statistics
10
+ * POST /profiles/:id/samples — upload a voice sample
11
+ *
12
+ * compose_in_voice uses POST /api/v1/mind/synthesize with
13
+ * synthesis_mode=crew_compose + voice_mode=voice_on.
14
+ */
15
+ import { z } from 'zod';
16
+ import { apiErrorResult } from './errors.js';
17
+ import { buildSynthesizeRequest } from './synthesize.js';
18
+ export function registerVoiceTools(server, getClientFn) {
19
+ // ── tacit_voice_list_profiles ──────────────────────────────────────────────
20
+ server.tool('tacit_voice_list_profiles', 'List your voice profiles with name, active status, sample count, and quality indicators. Use when someone asks about their voice profiles or writing style settings.', {}, async () => {
21
+ try {
22
+ const client = await getClientFn();
23
+ const result = await client.get('/api/v1/voice/profiles');
24
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
25
+ }
26
+ catch (err) {
27
+ return apiErrorResult(err, '/api/v1/voice/profiles');
28
+ }
29
+ });
30
+ // ── tacit_voice_get_profile ────────────────────────────────────────────────
31
+ server.tool('tacit_voice_get_profile', 'Get full details for a specific voice profile including constellation label, quality score, and configuration. Use when someone wants to inspect a particular voice profile.', {
32
+ profile_id: z.string().describe('Voice profile UUID'),
33
+ }, async ({ profile_id }) => {
34
+ try {
35
+ const client = await getClientFn();
36
+ const result = await client.get(`/api/v1/voice/profiles/${profile_id}`);
37
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
38
+ }
39
+ catch (err) {
40
+ return apiErrorResult(err, `/api/v1/voice/profiles/${profile_id}`);
41
+ }
42
+ });
43
+ // ── tacit_voice_get_stats ──────────────────────────────────────────────────
44
+ server.tool('tacit_voice_get_stats', 'Get statistics for a voice profile — sample count, quality score, constellation breakdown, and training status. Use when someone asks how good their voice profile is or wants quality details.', {
45
+ profile_id: z.string().describe('Voice profile UUID'),
46
+ }, async ({ profile_id }) => {
47
+ try {
48
+ const client = await getClientFn();
49
+ const result = await client.get(`/api/v1/voice/profiles/${profile_id}/stats`);
50
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
51
+ }
52
+ catch (err) {
53
+ return apiErrorResult(err, `/api/v1/voice/profiles/${profile_id}/stats`);
54
+ }
55
+ });
56
+ // ── tacit_voice_upload_sample ──────────────────────────────────────────────
57
+ server.tool('tacit_voice_upload_sample', 'Upload a text sample to train a voice profile. The sample teaches the system your writing style. Use when someone provides text they want added to their voice profile.', {
58
+ profile_id: z.string().describe('Voice profile UUID to add the sample to'),
59
+ content: z.string().min(1).describe('Text content of the writing sample'),
60
+ label: z.string().optional().describe('Optional label for the sample (e.g. "blog post", "email")'),
61
+ }, async ({ profile_id, content, label }) => {
62
+ try {
63
+ const client = await getClientFn();
64
+ const body = { content };
65
+ if (label)
66
+ body['label'] = label;
67
+ const result = await client.post(`/api/v1/voice/profiles/${profile_id}/samples`, body);
68
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
69
+ }
70
+ catch (err) {
71
+ return apiErrorResult(err, `/api/v1/voice/profiles/${profile_id}/samples`);
72
+ }
73
+ });
74
+ // ── tacit_compose_in_voice ─────────────────────────────────────────────────
75
+ server.tool('tacit_compose_in_voice', 'Compose an artifact in your writing voice. Combines crew-based artifact generation with voice personalization in a single call. Use when someone says "write this in my voice" or "compose a brief in my style".', {
76
+ query: z.string().min(1).describe('Composition prompt — what to write'),
77
+ voice_profile_id: z.string().optional().describe('Specific voice profile UUID. Omit to use the active profile.'),
78
+ scope: z.string().optional().describe('Optional scope filter (e.g. portfolio or program id)'),
79
+ workspace_id: z.string().optional().describe('Optional workspace UUID'),
80
+ collection_ids: z.array(z.string()).optional().describe('Optional collection UUIDs to scope synthesis to'),
81
+ program_ids: z.array(z.string()).optional().describe('Optional program UUIDs to scope synthesis to'),
82
+ }, async (args) => {
83
+ try {
84
+ const client = await getClientFn();
85
+ const input = {
86
+ query: args.query,
87
+ synthesis_mode: 'crew_compose',
88
+ voice_mode: 'voice_on',
89
+ voice_profile_id: args.voice_profile_id,
90
+ scope: args.scope,
91
+ workspace_id: args.workspace_id,
92
+ collection_ids: args.collection_ids,
93
+ program_ids: args.program_ids,
94
+ };
95
+ return buildSynthesizeRequest(client, input).execute();
96
+ }
97
+ catch (err) {
98
+ return apiErrorResult(err, 'compose_in_voice');
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Wiki governance tools — validate articles against governance rules,
3
+ * apply overrides, write to wiki repos, and author content via LLM.
4
+ *
5
+ * Routes (Axum, all under /api/v1/wiki):
6
+ * POST /validate — validate content against WX governance rules
7
+ * POST /override — propose or apply governance overrides (autonomy-tiered)
8
+ * POST /write — commit article to wiki repo (pre-validates)
9
+ * POST /author — generate or rewrite article via CLIProxy (W3)
10
+ */
11
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { TacitClient } from '../client.js';
13
+ export declare function registerWikiTools(server: McpServer, getClientFn: () => Promise<TacitClient>): void;
@@ -0,0 +1,166 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Wiki governance tools — validate articles against governance rules,
4
+ * apply overrides, write to wiki repos, and author content via LLM.
5
+ *
6
+ * Routes (Axum, all under /api/v1/wiki):
7
+ * POST /validate — validate content against WX governance rules
8
+ * POST /override — propose or apply governance overrides (autonomy-tiered)
9
+ * POST /write — commit article to wiki repo (pre-validates)
10
+ * POST /author — generate or rewrite article via CLIProxy (W3)
11
+ */
12
+ import { z } from 'zod';
13
+ import { TacitApiError, TokenExpiredError } from '../client.js';
14
+ function apiErrorResult(err, toolName) {
15
+ if (err instanceof TokenExpiredError) {
16
+ return { content: [{ type: 'text', text: err.message }], isError: true };
17
+ }
18
+ if (err instanceof TacitApiError) {
19
+ return { content: [{ type: 'text', text: `API error ${err.statusCode} on ${toolName}: ${err.body}` }], isError: true };
20
+ }
21
+ const msg = err instanceof Error ? err.message : String(err);
22
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
23
+ }
24
+ // ─── tacit_wiki_validate ───────────────────────────────────────────────────
25
+ export function registerWikiTools(server, getClientFn) {
26
+ server.tool('tacit_wiki_validate', 'Validate wiki article content against 33 governance rules (WX-01..WX-33). ' +
27
+ 'Returns verdict (PASS/WARN/FAIL), score breakdown (readability, structure, voice), ' +
28
+ 'Dale-Chall readability score, persona coherence score, and individual findings. ' +
29
+ 'Route: POST /api/v1/wiki/validate.', {
30
+ content: z.string().min(1).describe('Full article content (markdown with YAML frontmatter)'),
31
+ wiki: z.enum(['eng', 'delight']).describe('Target wiki: eng (engineering) or delight (user-facing)'),
32
+ persona: z.string().optional().describe('Persona key (developer, operator, end-user, decision-maker). Defaults to wiki default.'),
33
+ content_type: z.enum(['Tutorial', 'How-To', 'Reference', 'Explanation', 'Runbook']).optional().describe('Content type for template-specific rules. Falls back to frontmatter content_type field.'),
34
+ }, async (args) => {
35
+ const client = await getClientFn();
36
+ try {
37
+ const resp = await client.post('/api/v1/wiki/validate', {
38
+ content: args.content,
39
+ wiki: args.wiki,
40
+ persona: args.persona,
41
+ content_type: args.content_type,
42
+ });
43
+ return {
44
+ content: [{ type: 'text', text: JSON.stringify(resp, null, 2) }],
45
+ };
46
+ }
47
+ catch (err) {
48
+ return apiErrorResult(err, 'tacit_wiki_validate');
49
+ }
50
+ });
51
+ // ─── tacit_wiki_override ─────────────────────────────────────────────────
52
+ server.tool('tacit_wiki_override', 'Propose or apply a governance override for a specific rule on an article. ' +
53
+ 'Autonomy-tiered: SCRIBE=forbidden, MENTAT=proposes (24h expiry, needs TWIN approval), ' +
54
+ 'TWIN=applies directly (90-day expiry). Overrides are stored in article frontmatter. ' +
55
+ 'Route: POST /api/v1/wiki/override.', {
56
+ path: z.string().min(1).describe('Article path within the wiki (e.g., "architecture/data-model.mdx")'),
57
+ wiki: z.enum(['eng', 'delight']).describe('Target wiki'),
58
+ mode: z.enum(['per_finding', 'blanket_warn']).describe('Override a single rule or all Warn-severity findings'),
59
+ rule_id: z.string().optional().describe('Rule ID to override (e.g., "WX-01"). Required for per_finding mode.'),
60
+ reason: z.string().min(1).describe('Justification for the override'),
61
+ accept_proposal_id: z.string().uuid().optional().describe('UUID of a MENTAT-proposed override to accept (TWIN only). Links acceptance to the original proposal.'),
62
+ }, async (args) => {
63
+ const client = await getClientFn();
64
+ try {
65
+ const resp = await client.post('/api/v1/wiki/override', {
66
+ path: args.path,
67
+ wiki: args.wiki,
68
+ mode: args.mode,
69
+ rule_id: args.rule_id,
70
+ reason: args.reason,
71
+ accept_proposal_id: args.accept_proposal_id,
72
+ });
73
+ return {
74
+ content: [{ type: 'text', text: JSON.stringify(resp, null, 2) }],
75
+ };
76
+ }
77
+ catch (err) {
78
+ return apiErrorResult(err, 'tacit_wiki_override');
79
+ }
80
+ });
81
+ // ─── tacit_wiki_write ────────────────────────────────────────────────────
82
+ server.tool('tacit_wiki_write', 'Write an article to the wiki repository. Pre-validates content — rejects if any Error-severity ' +
83
+ 'findings exist. Commits to the wiki git repo (eng or delight) and returns the commit SHA. ' +
84
+ 'Returns 503 if the wiki repo is not available (CI/beta/prod environments). ' +
85
+ 'Route: POST /api/v1/wiki/write.', {
86
+ content: z.string().min(1).describe('Full article content (markdown with YAML frontmatter)'),
87
+ path: z.string().min(1).describe('File path within the wiki (e.g., "architecture/data-model.mdx")'),
88
+ wiki: z.enum(['eng', 'delight']).describe('Target wiki'),
89
+ commit_message: z.string().optional().describe('Git commit message (auto-generated if omitted)'),
90
+ }, async (args) => {
91
+ const client = await getClientFn();
92
+ try {
93
+ const resp = await client.post('/api/v1/wiki/write', {
94
+ content: args.content,
95
+ path: args.path,
96
+ wiki: args.wiki,
97
+ commit_message: args.commit_message,
98
+ });
99
+ return {
100
+ content: [{ type: 'text', text: JSON.stringify(resp, null, 2) }],
101
+ };
102
+ }
103
+ catch (err) {
104
+ return apiErrorResult(err, 'tacit_wiki_write');
105
+ }
106
+ });
107
+ // ─── tacit_wiki_classify ─────────────────────────────────────────────────
108
+ server.tool('tacit_wiki_classify', 'Classify wiki content: determine target wiki (eng/delight/split), content type ' +
109
+ '(Tutorial/How-To/Reference/Explanation/Runbook), recommended path, and persona. ' +
110
+ 'Uses LLM-backed Q1-Q4 decision tree. Requires CLIProxy and FEATURE_WIKI_CLASSIFY flag. ' +
111
+ 'Route: POST /api/v1/wiki/classify.', {
112
+ content: z.string().min(1).describe('Full article content to classify'),
113
+ wiki: z.string().optional().describe('Optional wiki hint (eng or delight). The classifier may override this.'),
114
+ }, async (args) => {
115
+ const client = await getClientFn();
116
+ try {
117
+ const resp = await client.post('/api/v1/wiki/classify', {
118
+ content: args.content,
119
+ wiki: args.wiki,
120
+ });
121
+ return {
122
+ content: [{ type: 'text', text: JSON.stringify(resp, null, 2) }],
123
+ };
124
+ }
125
+ catch (err) {
126
+ return apiErrorResult(err, 'tacit_wiki_classify');
127
+ }
128
+ });
129
+ // ─── tacit_wiki_author ───────────────────────────────────────────────────
130
+ server.tool('tacit_wiki_author', 'Generate or rewrite wiki content using CLIProxy LLM. Draft mode creates a new article ' +
131
+ 'from a topic; rewrite mode fixes governance findings while preserving content. ' +
132
+ 'Requires CLIProxy (port 8317) and FEATURE_WIKI_AUTHOR flag. ' +
133
+ 'Route: POST /api/v1/wiki/author.', {
134
+ mode: z.enum(['draft', 'rewrite']).describe('draft = new article from topic, rewrite = fix existing'),
135
+ wiki: z.enum(['eng', 'delight']).describe('Target wiki'),
136
+ persona: z.string().optional().describe('Persona key. Defaults to wiki default.'),
137
+ topic: z.string().optional().describe('Topic for draft mode'),
138
+ content: z.string().optional().describe('Existing content for rewrite mode'),
139
+ validation_findings: z.array(z.object({
140
+ rule_id: z.string(),
141
+ severity: z.enum(['WARN', 'ERROR']),
142
+ message: z.string(),
143
+ line: z.number().optional(),
144
+ overridable: z.boolean(),
145
+ category: z.enum(['frontmatter', 'structure', 'readability', 'voice', 'cross_link']),
146
+ })).optional().describe('Findings from tacit_wiki_validate to address in rewrite'),
147
+ }, async (args) => {
148
+ const client = await getClientFn();
149
+ try {
150
+ const resp = await client.post('/api/v1/wiki/author', {
151
+ mode: args.mode,
152
+ wiki: args.wiki,
153
+ persona: args.persona,
154
+ topic: args.topic,
155
+ content: args.content,
156
+ validation_findings: args.validation_findings,
157
+ });
158
+ return {
159
+ content: [{ type: 'text', text: JSON.stringify(resp, null, 2) }],
160
+ };
161
+ }
162
+ catch (err) {
163
+ return apiErrorResult(err, 'tacit_wiki_author');
164
+ }
165
+ });
166
+ }
package/dist/transport.js CHANGED
@@ -199,10 +199,11 @@ export async function startHttpTransport(server) {
199
199
  sendJson(res, 500, { error: 'internal error' });
200
200
  });
201
201
  });
202
+ const bindHost = process.env['MCP_BIND_HOST'] ?? '0.0.0.0';
202
203
  await new Promise((resolve, reject) => {
203
204
  httpServer.once('error', reject);
204
- httpServer.listen(port, '127.0.0.1', () => {
205
- process.stderr.write(`[tacit-mcp-http] listening on 127.0.0.1:${port}\n`);
205
+ httpServer.listen(port, bindHost, () => {
206
+ process.stderr.write(`[tacit-mcp-http] listening on ${bindHost}:${port}\n`);
206
207
  resolve();
207
208
  });
208
209
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacitai/mcp-server",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "stdio MCP server wrapping Tacit /api/v1 REST/SSE API",
5
5
  "license": "MIT",
6
6
  "repository": {