@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.
Files changed (70) hide show
  1. package/dist/auth.d.ts +32 -0
  2. package/dist/auth.js +64 -0
  3. package/dist/client.d.ts +60 -5
  4. package/dist/client.js +234 -53
  5. package/dist/index.js +130 -106
  6. package/dist/oauth.d.ts +115 -0
  7. package/dist/oauth.js +619 -0
  8. package/dist/prompts.d.ts +2 -0
  9. package/dist/prompts.js +12 -0
  10. package/dist/setup.d.ts +28 -5
  11. package/dist/setup.js +266 -59
  12. package/dist/tools/agents.d.ts +277 -0
  13. package/dist/tools/agents.js +335 -0
  14. package/dist/tools/annotations.d.ts +117 -0
  15. package/dist/tools/annotations.js +130 -0
  16. package/dist/tools/artifacts.d.ts +178 -8
  17. package/dist/tools/artifacts.js +372 -81
  18. package/dist/tools/chronicle.d.ts +3 -0
  19. package/dist/tools/chronicle.js +165 -0
  20. package/dist/tools/connector_write_back.d.ts +3 -0
  21. package/dist/tools/connector_write_back.js +27 -0
  22. package/dist/tools/converse.d.ts +10 -0
  23. package/dist/tools/converse.js +88 -0
  24. package/dist/tools/crews.d.ts +3 -0
  25. package/dist/tools/crews.js +115 -0
  26. package/dist/tools/drafts.d.ts +83 -0
  27. package/dist/tools/drafts.js +86 -0
  28. package/dist/tools/errors.d.ts +15 -0
  29. package/dist/tools/errors.js +21 -0
  30. package/dist/tools/export.d.ts +54 -0
  31. package/dist/tools/export.js +55 -0
  32. package/dist/tools/gm.d.ts +214 -0
  33. package/dist/tools/gm.js +198 -0
  34. package/dist/tools/ingest.d.ts +58 -4
  35. package/dist/tools/ingest.js +109 -76
  36. package/dist/tools/kb.d.ts +544 -0
  37. package/dist/tools/kb.js +687 -0
  38. package/dist/tools/lake-structure.d.ts +3 -0
  39. package/dist/tools/lake-structure.js +126 -0
  40. package/dist/tools/lake.d.ts +932 -0
  41. package/dist/tools/lake.js +1017 -0
  42. package/dist/tools/move.d.ts +200 -0
  43. package/dist/tools/move.js +96 -0
  44. package/dist/tools/outbox.d.ts +274 -0
  45. package/dist/tools/outbox.js +223 -0
  46. package/dist/tools/people.d.ts +3 -0
  47. package/dist/tools/people.js +59 -0
  48. package/dist/tools/pyramid.d.ts +554 -0
  49. package/dist/tools/pyramid.js +758 -0
  50. package/dist/tools/recall.d.ts +2 -0
  51. package/dist/tools/recall.js +28 -17
  52. package/dist/tools/registry.d.ts +17 -0
  53. package/dist/tools/registry.js +199 -0
  54. package/dist/tools/research.d.ts +47 -0
  55. package/dist/tools/research.js +575 -0
  56. package/dist/tools/search.d.ts +9 -0
  57. package/dist/tools/search.js +86 -25
  58. package/dist/tools/sharing.d.ts +63 -0
  59. package/dist/tools/sharing.js +157 -0
  60. package/dist/tools/skills.d.ts +9 -0
  61. package/dist/tools/skills.js +98 -0
  62. package/dist/tools/synthesize.d.ts +54 -2
  63. package/dist/tools/synthesize.js +80 -23
  64. package/dist/tools/web-url.d.ts +57 -0
  65. package/dist/tools/web-url.js +74 -0
  66. package/dist/tools/workspaces.d.ts +3 -0
  67. package/dist/tools/workspaces.js +47 -0
  68. package/dist/transport.d.ts +9 -0
  69. package/dist/transport.js +209 -0
  70. package/package.json +5 -4
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  // SPDX-License-Identifier: MIT
3
2
  /**
4
3
  * tacit_synthesize — canonical synthesis tool
@@ -9,18 +8,20 @@
9
8
  * Aliases: tacit_ask / tacit_compose route to this same handler.
10
9
  * stream:true selects the SSE variant and returns aggregated chunks.
11
10
  */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.SYNTHESIZE_TOOL_DEF = exports.synthesizeInputSchema = void 0;
14
- exports.buildSynthesizeRequest = buildSynthesizeRequest;
15
- exports.handleSynthesize = handleSynthesize;
16
- const zod_1 = require("zod");
17
- const client_js_1 = require("../client.js");
18
- exports.synthesizeInputSchema = zod_1.z.object({
19
- query: zod_1.z.string().min(1).describe('Question or synthesis prompt'),
20
- scope: zod_1.z.string().optional().describe('Optional scope filter (e.g. portfolio or program id)'),
21
- stream: zod_1.z.boolean().optional().describe('If true, use SSE stream variant (/mind/synthesize/stream)'),
11
+ import { z } from 'zod';
12
+ import { getClient, TacitApiError, TokenExpiredError } from '../client.js';
13
+ export const synthesizeInputSchema = z.object({
14
+ query: z.string().min(1).describe('Question or synthesis prompt'),
15
+ scope: z.string().optional().describe('Optional scope filter (e.g. portfolio or program id)'),
16
+ stream: z.boolean().optional().describe('If true, use SSE stream variant (/mind/synthesize/stream)'),
17
+ workspace_id: z.string().optional().describe('Optional workspace UUID'),
18
+ collection_ids: z.array(z.string()).optional().describe('Optional collection UUIDs to scope synthesis to'),
19
+ program_ids: z.array(z.string()).optional().describe('Optional program UUIDs to scope synthesis to'),
20
+ origin: z.string().optional().describe('Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results'),
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
+ 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).'),
22
23
  });
23
- exports.SYNTHESIZE_TOOL_DEF = {
24
+ export const SYNTHESIZE_TOOL_DEF = {
24
25
  name: 'tacit_synthesize',
25
26
  description: 'Synthesize knowledge from the Tacit workspace. ' +
26
27
  'stream:false (default) → POST /api/v1/mind/synthesize (JSON). ' +
@@ -31,6 +32,12 @@ exports.SYNTHESIZE_TOOL_DEF = {
31
32
  query: { type: 'string', description: 'Question or synthesis prompt' },
32
33
  scope: { type: 'string', description: 'Optional scope filter' },
33
34
  stream: { type: 'boolean', description: 'Use SSE stream variant if true' },
35
+ workspace_id: { type: 'string', description: 'Optional workspace UUID' },
36
+ collection_ids: { type: 'array', items: { type: 'string' }, description: 'Optional collection UUIDs to scope synthesis to' },
37
+ program_ids: { type: 'array', items: { type: 'string' }, description: 'Optional program UUIDs to scope synthesis to' },
38
+ origin: { type: 'string', description: 'Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results' },
39
+ 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
+ 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).' },
34
41
  },
35
42
  required: ['query'],
36
43
  },
@@ -39,29 +46,53 @@ exports.SYNTHESIZE_TOOL_DEF = {
39
46
  * Build a synthesize request object. Call .execute() to perform the HTTP call.
40
47
  * Exported for testability (allows injecting a mock TacitClient).
41
48
  */
42
- function buildSynthesizeRequest(client, input) {
49
+ export function buildSynthesizeRequest(client, input) {
43
50
  return {
44
51
  async execute() {
45
- const { query, scope, stream } = input;
52
+ const { query, scope, stream, workspace_id, collection_ids, program_ids, origin, synthesis_mode, max_turns } = input;
46
53
  const body = { query };
47
54
  if (scope)
48
55
  body['scope'] = scope;
56
+ if (workspace_id)
57
+ body['workspace_id'] = workspace_id;
58
+ if (collection_ids)
59
+ body['collection_ids'] = collection_ids;
60
+ if (program_ids)
61
+ body['program_ids'] = program_ids;
62
+ if (origin)
63
+ body['origin'] = origin;
64
+ if (synthesis_mode)
65
+ body['synthesis_mode'] = synthesis_mode;
66
+ if (max_turns)
67
+ body['max_turns'] = max_turns;
49
68
  try {
50
69
  if (stream) {
51
70
  // SSE path: POST /api/v1/mind/synthesize/stream
52
71
  const chunks = [];
72
+ let structuredAnswer = null;
53
73
  for await (const chunk of client.postSSE('/api/v1/mind/synthesize/stream', body)) {
74
+ try {
75
+ const parsed = JSON.parse(chunk);
76
+ if (parsed.type === 'structured_answer') {
77
+ structuredAnswer = parsed.data;
78
+ continue;
79
+ }
80
+ }
81
+ catch {
82
+ // not JSON — raw text chunk
83
+ }
54
84
  chunks.push(chunk);
55
85
  }
56
- if (chunks.length === 0) {
86
+ if (chunks.length === 0 && !structuredAnswer) {
57
87
  return {
58
88
  content: [{ type: 'text', text: JSON.stringify({ chunks: 0, text: '' }) }],
59
89
  };
60
90
  }
61
- // Aggregate: return all chunks as content items for D7 chunk counting.
62
- return {
63
- content: chunks.map((c) => ({ type: 'text', text: c })),
64
- };
91
+ const contentItems = chunks.map((c) => ({ type: 'text', text: c }));
92
+ if (structuredAnswer) {
93
+ contentItems.push({ type: 'text', text: JSON.stringify({ structured_answer: structuredAnswer }) });
94
+ }
95
+ return { content: contentItems };
65
96
  }
66
97
  else {
67
98
  // JSON path: POST /api/v1/mind/synthesize
@@ -72,7 +103,13 @@ function buildSynthesizeRequest(client, input) {
72
103
  }
73
104
  }
74
105
  catch (err) {
75
- if (err instanceof client_js_1.TacitApiError) {
106
+ if (err instanceof TokenExpiredError) {
107
+ return {
108
+ content: [{ type: 'text', text: err.message }],
109
+ isError: true,
110
+ };
111
+ }
112
+ if (err instanceof TacitApiError) {
76
113
  return { content: [{ type: 'text', text: `API error ${err.statusCode}: ${err.body}` }], isError: true };
77
114
  }
78
115
  const msg = err instanceof Error ? err.message : String(err);
@@ -81,13 +118,33 @@ function buildSynthesizeRequest(client, input) {
81
118
  },
82
119
  };
83
120
  }
84
- async function handleSynthesize(args) {
85
- const parsed = exports.synthesizeInputSchema.safeParse(args);
121
+ export async function handleSynthesize(args) {
122
+ const parsed = synthesizeInputSchema.safeParse(args);
86
123
  if (!parsed.success) {
87
124
  return {
88
125
  content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }],
89
126
  isError: true,
90
127
  };
91
128
  }
92
- return buildSynthesizeRequest((0, client_js_1.getClient)(), parsed.data).execute();
129
+ return buildSynthesizeRequest(getClient(), parsed.data).execute();
130
+ }
131
+ const SYNTH_SCHEMA = {
132
+ query: z.string().min(1).describe('Question or synthesis prompt'),
133
+ scope: z.string().optional().describe('Optional scope filter (e.g. portfolio or program id)'),
134
+ stream: z.boolean().optional().describe('If true, use SSE stream variant (/mind/synthesize/stream)'),
135
+ workspace_id: z.string().optional().describe('Optional workspace UUID'),
136
+ collection_ids: z.array(z.string()).optional().describe('Optional collection UUIDs to scope synthesis to'),
137
+ program_ids: z.array(z.string()).optional().describe('Optional program UUIDs to scope synthesis to'),
138
+ origin: z.string().optional().describe('Optional origin filter (e.g., PYRAMID, LAKE_SYNC, MANUAL) to scope results'),
139
+ 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
+ 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).'),
141
+ };
142
+ export function registerSynthesizeTools(server, getClientFn) {
143
+ const handler = async (args) => {
144
+ const client = await getClientFn();
145
+ return buildSynthesizeRequest(client, args).execute();
146
+ };
147
+ server.tool('tacit_synthesize', 'Synthesize knowledge from the Tacit workspace. synthesis_mode: burst (default, fast retrieval), orbit (agentic multi-turn), crew_compose (crew artifact generation). stream:false → JSON, stream:true → SSE.', SYNTH_SCHEMA, handler);
148
+ server.tool('tacit_ask', 'Ask a question against the Tacit workspace. Supports synthesis_mode: burst (default), orbit (agentic), crew_compose.', SYNTH_SCHEMA, handler);
149
+ server.tool('tacit_compose', 'Compose knowledge from the Tacit workspace. Supports synthesis_mode: burst (default), orbit (agentic), crew_compose.', SYNTH_SCHEMA, handler);
93
150
  }
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ export interface ToolRequest {
4
+ execute(): Promise<{
5
+ content: Array<{
6
+ type: 'text';
7
+ text: string;
8
+ }>;
9
+ isError?: boolean;
10
+ }>;
11
+ }
12
+ export declare const webUrlInputSchema: z.ZodObject<{
13
+ entity_type: z.ZodEnum<["artifact", "kb_article", "dossier", "dossier_summary", "dossier_scorecard", "dossier_deliverable", "guest", "knowledge"]>;
14
+ id: z.ZodString;
15
+ collection_id: z.ZodOptional<z.ZodString>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ id: string;
18
+ entity_type: "artifact" | "kb_article" | "dossier" | "dossier_summary" | "dossier_scorecard" | "dossier_deliverable" | "guest" | "knowledge";
19
+ collection_id?: string | undefined;
20
+ }, {
21
+ id: string;
22
+ entity_type: "artifact" | "kb_article" | "dossier" | "dossier_summary" | "dossier_scorecard" | "dossier_deliverable" | "guest" | "knowledge";
23
+ collection_id?: string | undefined;
24
+ }>;
25
+ export type WebUrlInput = z.infer<typeof webUrlInputSchema>;
26
+ export declare const WEB_URL_TOOL_DEF: {
27
+ name: "tacit_web_url";
28
+ description: string;
29
+ inputSchema: {
30
+ type: "object";
31
+ properties: {
32
+ entity_type: {
33
+ type: string;
34
+ enum: ("artifact" | "kb_article" | "dossier" | "dossier_summary" | "dossier_scorecard" | "dossier_deliverable" | "guest" | "knowledge")[];
35
+ description: string;
36
+ };
37
+ id: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ collection_id: {
42
+ type: string;
43
+ description: string;
44
+ };
45
+ };
46
+ required: string[];
47
+ };
48
+ };
49
+ export declare function buildWebUrlRequest(input: WebUrlInput): ToolRequest;
50
+ export declare function handleWebUrl(args: unknown): Promise<{
51
+ content: Array<{
52
+ type: 'text';
53
+ text: string;
54
+ }>;
55
+ isError?: boolean;
56
+ }>;
57
+ export declare function registerWebUrlTools(server: McpServer): void;
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { z } from 'zod';
3
+ import { getClient } from '../client.js';
4
+ const ENTITY_TYPES = [
5
+ 'artifact',
6
+ 'kb_article',
7
+ 'dossier',
8
+ 'dossier_summary',
9
+ 'dossier_scorecard',
10
+ 'dossier_deliverable',
11
+ 'guest',
12
+ 'knowledge',
13
+ ];
14
+ export const webUrlInputSchema = z.object({
15
+ entity_type: z.enum(ENTITY_TYPES).describe('Entity type: artifact, kb_article, dossier, dossier_summary, dossier_scorecard, dossier_deliverable, guest, knowledge'),
16
+ id: z.string().min(1).describe('UUID of the entity'),
17
+ collection_id: z.string().optional().describe('Required for kb_article (the parent collection UUID) and dossier_deliverable (the parent dossier UUID)'),
18
+ });
19
+ export const WEB_URL_TOOL_DEF = {
20
+ name: 'tacit_web_url',
21
+ description: 'Get the TACIT web app URL for an entity. Returns a direct browser link. ' +
22
+ 'Requires TACIT_APP_URL to be configured. For kb_article, collection_id is required.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ entity_type: {
27
+ type: 'string',
28
+ enum: [...ENTITY_TYPES],
29
+ description: 'Entity type',
30
+ },
31
+ id: { type: 'string', description: 'UUID of the entity' },
32
+ collection_id: { type: 'string', description: 'Parent collection UUID (required for kb_article, dossier_deliverable)' },
33
+ },
34
+ required: ['entity_type', 'id'],
35
+ },
36
+ };
37
+ export function buildWebUrlRequest(input) {
38
+ return {
39
+ async execute() {
40
+ const client = getClient();
41
+ const url = client.webUrl(input.entity_type, input.id, {
42
+ collectionId: input.collection_id,
43
+ });
44
+ if (!url) {
45
+ return {
46
+ content: [{
47
+ type: 'text',
48
+ text: 'TACIT_APP_URL is not configured. Set it in the MCP server environment or profile to enable web URLs.',
49
+ }],
50
+ isError: true,
51
+ };
52
+ }
53
+ return {
54
+ content: [{ type: 'text', text: JSON.stringify({ web_url: url, entity_type: input.entity_type, id: input.id }) }],
55
+ };
56
+ },
57
+ };
58
+ }
59
+ export async function handleWebUrl(args) {
60
+ const parsed = webUrlInputSchema.safeParse(args);
61
+ if (!parsed.success) {
62
+ return { content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }], isError: true };
63
+ }
64
+ return buildWebUrlRequest(parsed.data).execute();
65
+ }
66
+ export function registerWebUrlTools(server) {
67
+ server.tool('tacit_web_url', 'Get the TACIT web app URL for an entity.', {
68
+ entity_type: z.enum(ENTITY_TYPES).describe('Entity type'),
69
+ id: z.string().min(1).describe('UUID of the entity'),
70
+ collection_id: z.string().optional().describe('Parent collection UUID (required for kb_article, dossier_deliverable)'),
71
+ }, async (args) => {
72
+ return buildWebUrlRequest(args).execute();
73
+ });
74
+ }
@@ -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 registerWorkspaceTools(server: McpServer, getClient: () => Promise<TacitClient>): void;
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { TacitApiError } from '../client.js';
3
+ import { success, error } from './errors.js';
4
+ export function registerWorkspaceTools(server, getClient) {
5
+ server.tool('tacit_list_workspaces', 'List all workspaces accessible to the authenticated user.', {}, async () => {
6
+ try {
7
+ const client = await getClient();
8
+ const data = await client.request('GET', '/api/v1/workspaces/mine');
9
+ return success(data);
10
+ }
11
+ catch (e) {
12
+ if (e instanceof TacitApiError)
13
+ return error(e.message);
14
+ return error(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
15
+ }
16
+ });
17
+ server.tool('tacit_switch_workspace', 'Switch the active workspace. All subsequent tool calls will operate on the new workspace.', {
18
+ workspace_id: z.string().uuid().describe('UUID of the workspace to switch to'),
19
+ }, async (args) => {
20
+ try {
21
+ const client = await getClient();
22
+ const result = await client.switchWorkspace(args.workspace_id);
23
+ return success({
24
+ message: `Switched to workspace: ${result.name}`,
25
+ workspace_id: result.workspace_id,
26
+ name: result.name,
27
+ });
28
+ }
29
+ catch (e) {
30
+ if (e instanceof TacitApiError)
31
+ return error(e.message);
32
+ return error(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
33
+ }
34
+ });
35
+ server.tool('tacit_current_workspace', 'Get the currently active workspace ID and name.', {}, async () => {
36
+ try {
37
+ const client = await getClient();
38
+ return success({
39
+ workspace_id: client.workspaceId,
40
+ name: client.workspaceName,
41
+ });
42
+ }
43
+ catch (e) {
44
+ return error(`Unexpected error: ${e instanceof Error ? e.message : String(e)}`);
45
+ }
46
+ });
47
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * HTTPS (Streamable HTTP) transport for external MCP clients.
3
+ *
4
+ * Stateless: a fresh transport per request, caller's bearer token bound via
5
+ * AsyncLocalStorage. Requests are serialized because a single McpServer can
6
+ * only be attached to one transport at a time.
7
+ */
8
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ export declare function startHttpTransport(server: McpServer): Promise<void>;
@@ -0,0 +1,209 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * HTTPS (Streamable HTTP) transport for external MCP clients.
4
+ *
5
+ * Stateless: a fresh transport per request, caller's bearer token bound via
6
+ * AsyncLocalStorage. Requests are serialized because a single McpServer can
7
+ * only be attached to one transport at a time.
8
+ */
9
+ import * as http from 'node:http';
10
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
+ import { RateLimiter, resolveAuth } from './auth.js';
12
+ import { getBlockedReason, isBlockedOnHttps } from './tools/registry.js';
13
+ import { runWithRequestContext } from './client.js';
14
+ import { createHash } from 'node:crypto';
15
+ import { isOAuthEnabled, handleAuthorizeGet, handleAuthorizePost, handleTokenExchange, handleClientRegistration, handleDiscovery, handleProtectedResource, startCleanupInterval, } from './oauth.js';
16
+ const MAX_BODY_BYTES = 1_000_000;
17
+ function setCors(res, allowGet = false) {
18
+ res.setHeader('Access-Control-Allow-Origin', '*');
19
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
20
+ res.setHeader('Access-Control-Allow-Methods', allowGet ? 'GET, POST, OPTIONS' : 'POST, OPTIONS');
21
+ }
22
+ function sendJson(res, status, body) {
23
+ if (res.headersSent)
24
+ return;
25
+ res.writeHead(status, { 'Content-Type': 'application/json' });
26
+ res.end(JSON.stringify(body));
27
+ }
28
+ function send401(res, message, invalidToken = false) {
29
+ if (res.headersSent)
30
+ return;
31
+ const wwwAuth = invalidToken
32
+ ? 'Bearer error="invalid_token", error_description="The access token is expired or invalid"'
33
+ : 'Bearer';
34
+ res.writeHead(401, {
35
+ 'Content-Type': 'application/json',
36
+ 'WWW-Authenticate': wwwAuth,
37
+ });
38
+ res.end(JSON.stringify({ error: message }));
39
+ }
40
+ function rpcError(id, code, message) {
41
+ return { jsonrpc: '2.0', id: id ?? null, error: { code, message } };
42
+ }
43
+ function readBody(req) {
44
+ return new Promise((resolve, reject) => {
45
+ const chunks = [];
46
+ let size = 0;
47
+ req.on('data', (chunk) => {
48
+ size += chunk.length;
49
+ if (size > MAX_BODY_BYTES) {
50
+ reject(new Error('body too large'));
51
+ req.destroy();
52
+ return;
53
+ }
54
+ chunks.push(chunk);
55
+ });
56
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
57
+ req.on('error', reject);
58
+ });
59
+ }
60
+ function blockedToolCall(parsed) {
61
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
62
+ for (const msg of messages) {
63
+ if (msg && typeof msg === 'object' && msg['method'] === 'tools/call') {
64
+ const m = msg;
65
+ const name = typeof m.params?.name === 'string' ? m.params.name : '';
66
+ if (isBlockedOnHttps(name))
67
+ return { id: m.id, tool: name, reason: getBlockedReason(name) ?? 'unknown_tool' };
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ function getClientIp(req) {
73
+ return req.headers['cf-connecting-ip']
74
+ ?? req.headers['x-forwarded-for']?.split(',')[0]?.trim()
75
+ ?? req.socket.remoteAddress
76
+ ?? 'unknown';
77
+ }
78
+ export async function startHttpTransport(server) {
79
+ const port = Number.parseInt(process.env['MCP_HTTPS_PORT'] ?? '8319', 10);
80
+ const apiUrl = (process.env['TACIT_API_URL'] ?? 'http://127.0.0.1:7473').replace(/\/$/, '');
81
+ const externalUrl = (process.env['MCP_EXTERNAL_URL'] ?? `http://127.0.0.1:${port}`).replace(/\/$/, '');
82
+ const limiter = new RateLimiter();
83
+ if (isOAuthEnabled()) {
84
+ startCleanupInterval();
85
+ }
86
+ let queue = Promise.resolve();
87
+ const handle = async (req, res) => {
88
+ const pathname = (req.url ?? '/').split('?')[0];
89
+ process.stderr.write(`[tacit-mcp-http] ${req.method} ${pathname}\n`);
90
+ // ── OPTIONS (all paths) ─────────────────────────────────────────────
91
+ if (req.method === 'OPTIONS') {
92
+ const isOAuthPath = pathname === '/authorize' || pathname.startsWith('/.well-known/');
93
+ setCors(res, isOAuthPath);
94
+ res.writeHead(204);
95
+ res.end();
96
+ return;
97
+ }
98
+ // ── Health ───────────────────────────────────────────────────────────
99
+ if (req.method === 'GET' && pathname === '/health') {
100
+ sendJson(res, 200, { status: 'ok' });
101
+ return;
102
+ }
103
+ // ── Rate limit (all endpoints) ──────────────────────────────────────
104
+ const ip = getClientIp(req);
105
+ if (!limiter.tryConsume(ip)) {
106
+ sendJson(res, 429, { error: 'rate limit exceeded' });
107
+ return;
108
+ }
109
+ // ── OAuth endpoints (bypass serialization queue) ────────────────────
110
+ if (isOAuthEnabled()) {
111
+ if (pathname === '/.well-known/oauth-authorization-server' && req.method === 'GET') {
112
+ setCors(res, true);
113
+ handleDiscovery(res, externalUrl);
114
+ return;
115
+ }
116
+ if (pathname === '/.well-known/oauth-protected-resource' && req.method === 'GET') {
117
+ setCors(res, true);
118
+ handleProtectedResource(res, `${externalUrl}/mcp`, externalUrl);
119
+ return;
120
+ }
121
+ if (pathname === '/authorize' && req.method === 'GET') {
122
+ setCors(res, true);
123
+ await handleAuthorizeGet(req, res);
124
+ return;
125
+ }
126
+ if (pathname === '/authorize' && req.method === 'POST') {
127
+ await handleAuthorizePost(req, res, apiUrl);
128
+ return;
129
+ }
130
+ if (pathname === '/token' && req.method === 'POST') {
131
+ await handleTokenExchange(req, res);
132
+ return;
133
+ }
134
+ if (pathname === '/register' && req.method === 'POST') {
135
+ setCors(res);
136
+ await handleClientRegistration(req, res);
137
+ return;
138
+ }
139
+ }
140
+ // ── MCP endpoint ────────────────────────────────────────────────────
141
+ if (pathname !== '/mcp' || req.method !== 'POST') {
142
+ setCors(res);
143
+ sendJson(res, 404, { error: 'not found' });
144
+ return;
145
+ }
146
+ setCors(res);
147
+ const auth = resolveAuth(req);
148
+ if (!auth) {
149
+ send401(res, 'missing or empty Bearer token');
150
+ return;
151
+ }
152
+ if (auth.type === 'invalid_token') {
153
+ send401(res, 'invalid or expired OAuth token', true);
154
+ return;
155
+ }
156
+ let parsed;
157
+ try {
158
+ parsed = JSON.parse(await readBody(req));
159
+ }
160
+ catch {
161
+ sendJson(res, 400, rpcError(null, -32700, 'Parse error'));
162
+ return;
163
+ }
164
+ const blocked = blockedToolCall(parsed);
165
+ if (blocked) {
166
+ sendJson(res, 200, rpcError(blocked.id, -32601, `Tool not available over HTTPS: ${blocked.tool} (reason: ${blocked.reason})`));
167
+ return;
168
+ }
169
+ const requestToken = auth.type === 'oauth' ? auth.info.tacit_token : auth.token;
170
+ const principal = auth.type === 'oauth'
171
+ ? auth.info.user_email
172
+ : createHash('sha256').update(auth.token).digest('hex').slice(0, 16);
173
+ const run = async () => {
174
+ const transport = new StreamableHTTPServerTransport({
175
+ sessionIdGenerator: undefined,
176
+ enableJsonResponse: true,
177
+ });
178
+ const done = new Promise((resolve) => res.on('close', () => resolve()));
179
+ try {
180
+ await server.connect(transport);
181
+ await runWithRequestContext(requestToken, apiUrl, () => transport.handleRequest(req, res, parsed), principal);
182
+ await done;
183
+ }
184
+ catch (err) {
185
+ process.stderr.write(`[tacit-mcp-http] request error: ${err instanceof Error ? err.message : String(err)}\n`);
186
+ sendJson(res, 500, rpcError(null, -32603, 'Internal error'));
187
+ }
188
+ finally {
189
+ await transport.close().catch(() => undefined);
190
+ }
191
+ };
192
+ const next = queue.then(run, run);
193
+ queue = next.catch(() => undefined);
194
+ await next;
195
+ };
196
+ const httpServer = http.createServer((req, res) => {
197
+ handle(req, res).catch((err) => {
198
+ process.stderr.write(`[tacit-mcp-http] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
199
+ sendJson(res, 500, { error: 'internal error' });
200
+ });
201
+ });
202
+ await new Promise((resolve, reject) => {
203
+ 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`);
206
+ resolve();
207
+ });
208
+ });
209
+ }
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "@tacitai/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "stdio MCP server wrapping Tacit /api/v1 REST/SSE API",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/scottkmcmillan/tacit.git",
8
+ "url": "git+https://github.com/scottkmcmillan/tacit.git",
9
9
  "directory": "packages/tacit-mcp-server"
10
10
  },
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "type": "module",
14
15
  "main": "dist/index.js",
15
16
  "bin": {
16
- "tacit-mcp-server": "./dist/index.js"
17
+ "tacit-mcp-server": "dist/index.js"
17
18
  },
18
19
  "files": [
19
20
  "dist",
@@ -31,7 +32,7 @@
31
32
  "test": "vitest run"
32
33
  },
33
34
  "dependencies": {
34
- "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "@modelcontextprotocol/sdk": "^1.12.0",
35
36
  "semver": "^7.6.0",
36
37
  "zod": "^3.22.0"
37
38
  },