@tacitai/mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +265 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +168 -0
- package/dist/setup.d.ts +18 -0
- package/dist/setup.js +211 -0
- package/dist/tools/artifacts.d.ts +208 -0
- package/dist/tools/artifacts.js +241 -0
- package/dist/tools/ingest.d.ts +139 -0
- package/dist/tools/ingest.js +259 -0
- package/dist/tools/recall.d.ts +63 -0
- package/dist/tools/recall.js +66 -0
- package/dist/tools/search.d.ts +77 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/synthesize.d.ts +68 -0
- package/dist/tools/synthesize.js +93 -0
- package/package.json +44 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest tools:
|
|
3
|
+
* tacit_ingest_file — POST /api/v1/portfolios/:id/ingest
|
|
4
|
+
* tacit_ingest_url — POST /api/v1/portfolios/:id/ingest-url
|
|
5
|
+
*
|
|
6
|
+
* Both require portfolio_id input.
|
|
7
|
+
*
|
|
8
|
+
* tacit_ingest_url SSRF guard (mandatory, runs BEFORE any network call):
|
|
9
|
+
* 1. Scheme must be https:// only.
|
|
10
|
+
* 2. Host must NOT be loopback (127.0.0.0/8, ::1, localhost) or RFC-1918 private
|
|
11
|
+
* (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) or link-local (169.254.0.0/16).
|
|
12
|
+
* Returns MCP error result (not a throw) on violation.
|
|
13
|
+
*
|
|
14
|
+
* The guard is synchronous and checks literal IPs/hostnames in the URL
|
|
15
|
+
* (no DNS resolution required for the guard to function).
|
|
16
|
+
*/
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { TacitClient } from '../client.js';
|
|
19
|
+
export interface ToolRequest {
|
|
20
|
+
execute(): Promise<{
|
|
21
|
+
content: Array<{
|
|
22
|
+
type: 'text';
|
|
23
|
+
text: string;
|
|
24
|
+
}>;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Synchronous SSRF guard for ingest_url.
|
|
30
|
+
* Returns { ok: true } if the URL is safe, or { ok: false, reason: string } if not.
|
|
31
|
+
*
|
|
32
|
+
* Exported as `validateIngestUrl` for direct use by tests (ssrf-guard.test.ts).
|
|
33
|
+
*
|
|
34
|
+
* SECURITY — DEFENSE IN DEPTH (M1, MV-1057c):
|
|
35
|
+
* This guard inspects the LITERAL host string in the URL only (scheme +
|
|
36
|
+
* literal-IP / hostname classification). It does NOT perform DNS resolution,
|
|
37
|
+
* so it does NOT defend against DNS-rebinding: a public hostname that resolves
|
|
38
|
+
* to a private/loopback IP (e.g. 169.254.169.254 via a rebind record) would
|
|
39
|
+
* PASS this client-side check. This is intentional and accepted — this layer
|
|
40
|
+
* is a fast client-side first line of defense, not the authoritative SSRF
|
|
41
|
+
* boundary. DNS-rebinding and resolve-to-private-IP egress is mitigated at the
|
|
42
|
+
* cell's egress network policy (the D-10 egress chokepoint), which enforces
|
|
43
|
+
* destination-IP filtering at request time, after resolution. Do not rely on
|
|
44
|
+
* this function alone for SSRF protection.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateIngestUrl(rawUrl: string): {
|
|
47
|
+
ok: true;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: string;
|
|
51
|
+
};
|
|
52
|
+
export declare const ingestFileInputSchema: z.ZodObject<{
|
|
53
|
+
portfolio_id: z.ZodString;
|
|
54
|
+
path: z.ZodString;
|
|
55
|
+
title: z.ZodOptional<z.ZodString>;
|
|
56
|
+
}, "strip", z.ZodTypeAny, {
|
|
57
|
+
path: string;
|
|
58
|
+
portfolio_id: string;
|
|
59
|
+
title?: string | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
path: string;
|
|
62
|
+
portfolio_id: string;
|
|
63
|
+
title?: string | undefined;
|
|
64
|
+
}>;
|
|
65
|
+
export type IngestFileInput = z.infer<typeof ingestFileInputSchema>;
|
|
66
|
+
export declare const INGEST_FILE_TOOL_DEF: {
|
|
67
|
+
name: "tacit_ingest_file";
|
|
68
|
+
description: string;
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object";
|
|
71
|
+
properties: {
|
|
72
|
+
portfolio_id: {
|
|
73
|
+
type: string;
|
|
74
|
+
description: string;
|
|
75
|
+
};
|
|
76
|
+
path: {
|
|
77
|
+
type: string;
|
|
78
|
+
description: string;
|
|
79
|
+
};
|
|
80
|
+
title: {
|
|
81
|
+
type: string;
|
|
82
|
+
description: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
required: string[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export declare function buildIngestFileRequest(client: TacitClient, input: IngestFileInput): ToolRequest;
|
|
89
|
+
export declare function handleIngestFile(args: unknown): Promise<{
|
|
90
|
+
content: Array<{
|
|
91
|
+
type: 'text';
|
|
92
|
+
text: string;
|
|
93
|
+
}>;
|
|
94
|
+
isError?: boolean;
|
|
95
|
+
}>;
|
|
96
|
+
export declare const ingestUrlInputSchema: z.ZodObject<{
|
|
97
|
+
portfolio_id: z.ZodString;
|
|
98
|
+
url: z.ZodString;
|
|
99
|
+
program_id: z.ZodString;
|
|
100
|
+
}, "strip", z.ZodTypeAny, {
|
|
101
|
+
portfolio_id: string;
|
|
102
|
+
url: string;
|
|
103
|
+
program_id: string;
|
|
104
|
+
}, {
|
|
105
|
+
portfolio_id: string;
|
|
106
|
+
url: string;
|
|
107
|
+
program_id: string;
|
|
108
|
+
}>;
|
|
109
|
+
export type IngestUrlInput = z.infer<typeof ingestUrlInputSchema>;
|
|
110
|
+
export declare const INGEST_URL_TOOL_DEF: {
|
|
111
|
+
name: "tacit_ingest_url";
|
|
112
|
+
description: string;
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object";
|
|
115
|
+
properties: {
|
|
116
|
+
portfolio_id: {
|
|
117
|
+
type: string;
|
|
118
|
+
description: string;
|
|
119
|
+
};
|
|
120
|
+
url: {
|
|
121
|
+
type: string;
|
|
122
|
+
description: string;
|
|
123
|
+
};
|
|
124
|
+
program_id: {
|
|
125
|
+
type: string;
|
|
126
|
+
description: string;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
required: string[];
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
export declare function buildIngestUrlRequest(client: TacitClient, input: IngestUrlInput): ToolRequest;
|
|
133
|
+
export declare function handleIngestUrl(args: unknown): Promise<{
|
|
134
|
+
content: Array<{
|
|
135
|
+
type: 'text';
|
|
136
|
+
text: string;
|
|
137
|
+
}>;
|
|
138
|
+
isError?: boolean;
|
|
139
|
+
}>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Ingest tools:
|
|
5
|
+
* tacit_ingest_file — POST /api/v1/portfolios/:id/ingest
|
|
6
|
+
* tacit_ingest_url — POST /api/v1/portfolios/:id/ingest-url
|
|
7
|
+
*
|
|
8
|
+
* Both require portfolio_id input.
|
|
9
|
+
*
|
|
10
|
+
* tacit_ingest_url SSRF guard (mandatory, runs BEFORE any network call):
|
|
11
|
+
* 1. Scheme must be https:// only.
|
|
12
|
+
* 2. Host must NOT be loopback (127.0.0.0/8, ::1, localhost) or RFC-1918 private
|
|
13
|
+
* (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) or link-local (169.254.0.0/16).
|
|
14
|
+
* Returns MCP error result (not a throw) on violation.
|
|
15
|
+
*
|
|
16
|
+
* The guard is synchronous and checks literal IPs/hostnames in the URL
|
|
17
|
+
* (no DNS resolution required for the guard to function).
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.INGEST_URL_TOOL_DEF = exports.ingestUrlInputSchema = exports.INGEST_FILE_TOOL_DEF = exports.ingestFileInputSchema = void 0;
|
|
54
|
+
exports.validateIngestUrl = validateIngestUrl;
|
|
55
|
+
exports.buildIngestFileRequest = buildIngestFileRequest;
|
|
56
|
+
exports.handleIngestFile = handleIngestFile;
|
|
57
|
+
exports.buildIngestUrlRequest = buildIngestUrlRequest;
|
|
58
|
+
exports.handleIngestUrl = handleIngestUrl;
|
|
59
|
+
const zod_1 = require("zod");
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const client_js_1 = require("../client.js");
|
|
63
|
+
// ─── SSRF guard — synchronous IP/hostname check ───────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Parse an IPv4 string into octets, returning null if invalid.
|
|
66
|
+
*/
|
|
67
|
+
function parseIPv4(host) {
|
|
68
|
+
const parts = host.split('.');
|
|
69
|
+
if (parts.length !== 4)
|
|
70
|
+
return null;
|
|
71
|
+
const octets = parts.map(Number);
|
|
72
|
+
if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255))
|
|
73
|
+
return null;
|
|
74
|
+
return octets;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns true if the host is a private/loopback/link-local address.
|
|
78
|
+
* Checks literal IPs and the "localhost" hostname.
|
|
79
|
+
*/
|
|
80
|
+
function isPrivateHost(host) {
|
|
81
|
+
// Strip brackets from IPv6 literals like [::1]
|
|
82
|
+
const raw = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
83
|
+
// localhost hostname
|
|
84
|
+
if (raw === 'localhost')
|
|
85
|
+
return true;
|
|
86
|
+
// IPv6 loopback
|
|
87
|
+
if (raw === '::1' || raw === '0:0:0:0:0:0:0:1')
|
|
88
|
+
return true;
|
|
89
|
+
// IPv4 checks
|
|
90
|
+
const octets = parseIPv4(raw);
|
|
91
|
+
if (octets) {
|
|
92
|
+
const [a, b] = octets;
|
|
93
|
+
if (a === 127)
|
|
94
|
+
return true; // 127.0.0.0/8 loopback
|
|
95
|
+
if (a === 10)
|
|
96
|
+
return true; // 10.0.0.0/8
|
|
97
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
98
|
+
return true; // 172.16.0.0/12
|
|
99
|
+
if (a === 192 && b === 168)
|
|
100
|
+
return true; // 192.168.0.0/16
|
|
101
|
+
if (a === 169 && b === 254)
|
|
102
|
+
return true; // 169.254.0.0/16 link-local (AWS IMDS)
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Synchronous SSRF guard for ingest_url.
|
|
108
|
+
* Returns { ok: true } if the URL is safe, or { ok: false, reason: string } if not.
|
|
109
|
+
*
|
|
110
|
+
* Exported as `validateIngestUrl` for direct use by tests (ssrf-guard.test.ts).
|
|
111
|
+
*
|
|
112
|
+
* SECURITY — DEFENSE IN DEPTH (M1, MV-1057c):
|
|
113
|
+
* This guard inspects the LITERAL host string in the URL only (scheme +
|
|
114
|
+
* literal-IP / hostname classification). It does NOT perform DNS resolution,
|
|
115
|
+
* so it does NOT defend against DNS-rebinding: a public hostname that resolves
|
|
116
|
+
* to a private/loopback IP (e.g. 169.254.169.254 via a rebind record) would
|
|
117
|
+
* PASS this client-side check. This is intentional and accepted — this layer
|
|
118
|
+
* is a fast client-side first line of defense, not the authoritative SSRF
|
|
119
|
+
* boundary. DNS-rebinding and resolve-to-private-IP egress is mitigated at the
|
|
120
|
+
* cell's egress network policy (the D-10 egress chokepoint), which enforces
|
|
121
|
+
* destination-IP filtering at request time, after resolution. Do not rely on
|
|
122
|
+
* this function alone for SSRF protection.
|
|
123
|
+
*/
|
|
124
|
+
function validateIngestUrl(rawUrl) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = new URL(rawUrl);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { ok: false, reason: 'URL is not valid' };
|
|
131
|
+
}
|
|
132
|
+
// 1. Scheme must be https://
|
|
133
|
+
if (parsed.protocol !== 'https:') {
|
|
134
|
+
return { ok: false, reason: `URL scheme must be https:// (got ${parsed.protocol.replace(/:$/, '')})` };
|
|
135
|
+
}
|
|
136
|
+
// 2. Host must not be private/loopback/link-local
|
|
137
|
+
const hostname = parsed.hostname;
|
|
138
|
+
if (isPrivateHost(hostname)) {
|
|
139
|
+
return { ok: false, reason: `URL host "${hostname}" resolves to a private/loopback/link-local address` };
|
|
140
|
+
}
|
|
141
|
+
return { ok: true };
|
|
142
|
+
}
|
|
143
|
+
// ─── tacit_ingest_file ────────────────────────────────────────────────────────
|
|
144
|
+
exports.ingestFileInputSchema = zod_1.z.object({
|
|
145
|
+
portfolio_id: zod_1.z.string().min(1).describe('Portfolio UUID to ingest into'),
|
|
146
|
+
path: zod_1.z.string().min(1).describe('Local file path to ingest'),
|
|
147
|
+
title: zod_1.z.string().optional().describe('Optional title override'),
|
|
148
|
+
});
|
|
149
|
+
exports.INGEST_FILE_TOOL_DEF = {
|
|
150
|
+
name: 'tacit_ingest_file',
|
|
151
|
+
description: 'Ingest a local file into a Tacit portfolio. Route: POST /api/v1/portfolios/:id/ingest.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
portfolio_id: { type: 'string', description: 'Portfolio UUID' },
|
|
156
|
+
path: { type: 'string', description: 'Local file path to ingest' },
|
|
157
|
+
title: { type: 'string', description: 'Optional title override' },
|
|
158
|
+
},
|
|
159
|
+
required: ['portfolio_id', 'path'],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
function buildIngestFileRequest(client, input) {
|
|
163
|
+
return {
|
|
164
|
+
async execute() {
|
|
165
|
+
const { portfolio_id, path: filePath, title } = input;
|
|
166
|
+
try {
|
|
167
|
+
const resolvedPath = path.resolve(filePath);
|
|
168
|
+
// POST /api/v1/portfolios/:id/ingest expects JSON { content: string, title?: string }.
|
|
169
|
+
// Read the file content as text if the file exists; fall back to the path as content.
|
|
170
|
+
let content;
|
|
171
|
+
if (fs.existsSync(resolvedPath)) {
|
|
172
|
+
content = fs.readFileSync(resolvedPath, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
content = filePath;
|
|
176
|
+
}
|
|
177
|
+
const body = { content };
|
|
178
|
+
if (title)
|
|
179
|
+
body['title'] = title;
|
|
180
|
+
// POST /api/v1/portfolios/:id/ingest
|
|
181
|
+
const result = await client.post(`/api/v1/portfolios/${portfolio_id}/ingest`, body);
|
|
182
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return apiErrorResult(err, `/api/v1/portfolios/:id/ingest`);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function handleIngestFile(args) {
|
|
191
|
+
const parsed = exports.ingestFileInputSchema.safeParse(args);
|
|
192
|
+
if (!parsed.success) {
|
|
193
|
+
return { content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }], isError: true };
|
|
194
|
+
}
|
|
195
|
+
return buildIngestFileRequest((0, client_js_1.getClient)(), parsed.data).execute();
|
|
196
|
+
}
|
|
197
|
+
// ─── tacit_ingest_url ─────────────────────────────────────────────────────────
|
|
198
|
+
exports.ingestUrlInputSchema = zod_1.z.object({
|
|
199
|
+
portfolio_id: zod_1.z.string().min(1).describe('Portfolio UUID to ingest into'),
|
|
200
|
+
url: zod_1.z.string().min(1).describe('Public HTTPS URL to ingest (must be https://, no private/loopback hosts)'),
|
|
201
|
+
program_id: zod_1.z.string().min(1).describe('Program UUID within the portfolio'),
|
|
202
|
+
});
|
|
203
|
+
exports.INGEST_URL_TOOL_DEF = {
|
|
204
|
+
name: 'tacit_ingest_url',
|
|
205
|
+
description: 'Ingest a public URL into a Tacit portfolio. Route: POST /api/v1/portfolios/:id/ingest-url. ' +
|
|
206
|
+
'URL must use https:// scheme and must not resolve to private/loopback addresses (SSRF protection).',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
portfolio_id: { type: 'string', description: 'Portfolio UUID' },
|
|
211
|
+
url: { type: 'string', description: 'Public HTTPS URL to ingest' },
|
|
212
|
+
program_id: { type: 'string', description: 'Program UUID within the portfolio' },
|
|
213
|
+
},
|
|
214
|
+
required: ['portfolio_id', 'url', 'program_id'],
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
function buildIngestUrlRequest(client, input) {
|
|
218
|
+
return {
|
|
219
|
+
async execute() {
|
|
220
|
+
const { portfolio_id, url, program_id } = input;
|
|
221
|
+
// SSRF guard — synchronous check runs BEFORE any network call.
|
|
222
|
+
const guard = validateIngestUrl(url);
|
|
223
|
+
if (!guard.ok) {
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: 'text', text: `SSRF guard rejected URL: ${guard.reason}` }],
|
|
226
|
+
isError: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
// Backend IngestUrlRequest accepts only source_url + program_id (title is dropped).
|
|
231
|
+
const body = { source_url: url, program_id };
|
|
232
|
+
// POST /api/v1/portfolios/:id/ingest-url
|
|
233
|
+
const result = await client.post(`/api/v1/portfolios/${portfolio_id}/ingest-url`, body);
|
|
234
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return apiErrorResult(err, `/api/v1/portfolios/:id/ingest-url`);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function handleIngestUrl(args) {
|
|
243
|
+
const parsed = exports.ingestUrlInputSchema.safeParse(args);
|
|
244
|
+
if (!parsed.success) {
|
|
245
|
+
return { content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }], isError: true };
|
|
246
|
+
}
|
|
247
|
+
return buildIngestUrlRequest((0, client_js_1.getClient)(), parsed.data).execute();
|
|
248
|
+
}
|
|
249
|
+
// ─── Shared error helper ──────────────────────────────────────────────────────
|
|
250
|
+
function apiErrorResult(err, endpoint) {
|
|
251
|
+
if (err instanceof client_js_1.TacitApiError) {
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: 'text', text: `API error ${err.statusCode} on ${endpoint}: ${err.body}` }],
|
|
254
|
+
isError: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
258
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
259
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tacit_recall — Chronicle event recall (hybrid vector)
|
|
3
|
+
* Route: POST /api/v1/mind/recall
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { TacitClient } from '../client.js';
|
|
7
|
+
export declare const recallInputSchema: z.ZodObject<{
|
|
8
|
+
query: z.ZodString;
|
|
9
|
+
portfolio_id: z.ZodString;
|
|
10
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
query: string;
|
|
13
|
+
portfolio_id: string;
|
|
14
|
+
limit?: number | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
query: string;
|
|
17
|
+
portfolio_id: string;
|
|
18
|
+
limit?: number | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
export type RecallInput = z.infer<typeof recallInputSchema>;
|
|
21
|
+
export declare const RECALL_TOOL_DEF: {
|
|
22
|
+
name: "tacit_recall";
|
|
23
|
+
description: string;
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object";
|
|
26
|
+
properties: {
|
|
27
|
+
query: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
portfolio_id: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
limit: {
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
required: string[];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export interface ToolRequest {
|
|
44
|
+
execute(): Promise<{
|
|
45
|
+
content: Array<{
|
|
46
|
+
type: 'text';
|
|
47
|
+
text: string;
|
|
48
|
+
}>;
|
|
49
|
+
isError?: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build a recall request object. Call .execute() to perform the HTTP call.
|
|
54
|
+
* Exported for testability (allows injecting a mock TacitClient).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildRecallRequest(client: TacitClient, input: RecallInput): ToolRequest;
|
|
57
|
+
export declare function handleRecall(args: unknown): Promise<{
|
|
58
|
+
content: Array<{
|
|
59
|
+
type: 'text';
|
|
60
|
+
text: string;
|
|
61
|
+
}>;
|
|
62
|
+
isError?: boolean;
|
|
63
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* tacit_recall — Chronicle event recall (hybrid vector)
|
|
5
|
+
* Route: POST /api/v1/mind/recall
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RECALL_TOOL_DEF = exports.recallInputSchema = void 0;
|
|
9
|
+
exports.buildRecallRequest = buildRecallRequest;
|
|
10
|
+
exports.handleRecall = handleRecall;
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const client_js_1 = require("../client.js");
|
|
13
|
+
exports.recallInputSchema = zod_1.z.object({
|
|
14
|
+
query: zod_1.z.string().min(1).describe('Recall query'),
|
|
15
|
+
portfolio_id: zod_1.z.string().min(1).describe('Portfolio UUID to scope the recall'),
|
|
16
|
+
limit: zod_1.z.number().int().positive().optional().describe('Maximum events to return'),
|
|
17
|
+
});
|
|
18
|
+
exports.RECALL_TOOL_DEF = {
|
|
19
|
+
name: 'tacit_recall',
|
|
20
|
+
description: 'Recall Chronicle events from Tacit workspace via hybrid vector search. Route: POST /api/v1/mind/recall.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
query: { type: 'string', description: 'Recall query' },
|
|
25
|
+
portfolio_id: { type: 'string', description: 'Portfolio UUID to scope the recall' },
|
|
26
|
+
limit: { type: 'number', description: 'Maximum events to return' },
|
|
27
|
+
},
|
|
28
|
+
required: ['query', 'portfolio_id'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Build a recall request object. Call .execute() to perform the HTTP call.
|
|
33
|
+
* Exported for testability (allows injecting a mock TacitClient).
|
|
34
|
+
*/
|
|
35
|
+
function buildRecallRequest(client, input) {
|
|
36
|
+
return {
|
|
37
|
+
async execute() {
|
|
38
|
+
const { query, portfolio_id, limit } = input;
|
|
39
|
+
try {
|
|
40
|
+
const body = { query, portfolio_id };
|
|
41
|
+
if (limit !== undefined)
|
|
42
|
+
body['limit'] = limit;
|
|
43
|
+
// POST /api/v1/mind/recall
|
|
44
|
+
const result = await client.post('/api/v1/mind/recall', body);
|
|
45
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err instanceof client_js_1.TacitApiError) {
|
|
49
|
+
return { content: [{ type: 'text', text: `API error ${err.statusCode}: ${err.body}` }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function handleRecall(args) {
|
|
58
|
+
const parsed = exports.recallInputSchema.safeParse(args);
|
|
59
|
+
if (!parsed.success) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return buildRecallRequest((0, client_js_1.getClient)(), parsed.data).execute();
|
|
66
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tacit_search — hybrid BM25+ANN+RRF search
|
|
3
|
+
* Route: GET /api/v1/search?q=<term>
|
|
4
|
+
* JWT-scoped; workspace resolved by TacitClient.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { TacitClient } from '../client.js';
|
|
8
|
+
export declare const searchInputSchema: z.ZodObject<{
|
|
9
|
+
query: z.ZodString;
|
|
10
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
source_types: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
12
|
+
entity_types: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
query: string;
|
|
15
|
+
limit?: number | undefined;
|
|
16
|
+
source_types?: string[] | undefined;
|
|
17
|
+
entity_types?: string[] | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
query: string;
|
|
20
|
+
limit?: number | undefined;
|
|
21
|
+
source_types?: string[] | undefined;
|
|
22
|
+
entity_types?: string[] | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export type SearchInput = z.infer<typeof searchInputSchema>;
|
|
25
|
+
export declare const SEARCH_TOOL_DEF: {
|
|
26
|
+
name: "tacit_search";
|
|
27
|
+
description: string;
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object";
|
|
30
|
+
properties: {
|
|
31
|
+
query: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
limit: {
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
source_types: {
|
|
40
|
+
type: string;
|
|
41
|
+
items: {
|
|
42
|
+
type: string;
|
|
43
|
+
};
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
entity_types: {
|
|
47
|
+
type: string;
|
|
48
|
+
items: {
|
|
49
|
+
type: string;
|
|
50
|
+
};
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
required: string[];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export interface ToolRequest {
|
|
58
|
+
execute(): Promise<{
|
|
59
|
+
content: Array<{
|
|
60
|
+
type: 'text';
|
|
61
|
+
text: string;
|
|
62
|
+
}>;
|
|
63
|
+
isError?: boolean;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build a search request object. Call .execute() to perform the HTTP call.
|
|
68
|
+
* Exported for testability (allows injecting a mock TacitClient).
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildSearchRequest(client: TacitClient, input: SearchInput): ToolRequest;
|
|
71
|
+
export declare function handleSearch(args: unknown): Promise<{
|
|
72
|
+
content: Array<{
|
|
73
|
+
type: 'text';
|
|
74
|
+
text: string;
|
|
75
|
+
}>;
|
|
76
|
+
isError?: boolean;
|
|
77
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* tacit_search — hybrid BM25+ANN+RRF search
|
|
5
|
+
* Route: GET /api/v1/search?q=<term>
|
|
6
|
+
* JWT-scoped; workspace resolved by TacitClient.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SEARCH_TOOL_DEF = exports.searchInputSchema = void 0;
|
|
10
|
+
exports.buildSearchRequest = buildSearchRequest;
|
|
11
|
+
exports.handleSearch = handleSearch;
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const client_js_1 = require("../client.js");
|
|
14
|
+
exports.searchInputSchema = zod_1.z.object({
|
|
15
|
+
query: zod_1.z.string().min(1).describe('Search query string'),
|
|
16
|
+
limit: zod_1.z.number().int().positive().optional().describe('Maximum results to return'),
|
|
17
|
+
source_types: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by source types (e.g. ["artifact","chronicle"])'),
|
|
18
|
+
entity_types: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by entity types'),
|
|
19
|
+
});
|
|
20
|
+
exports.SEARCH_TOOL_DEF = {
|
|
21
|
+
name: 'tacit_search',
|
|
22
|
+
description: 'Search Tacit knowledge using hybrid BM25+ANN+RRF against /api/v1/search. Returns ranked results from the workspace corpus.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
query: { type: 'string', description: 'Search query string' },
|
|
27
|
+
limit: { type: 'number', description: 'Maximum results to return' },
|
|
28
|
+
source_types: { type: 'array', items: { type: 'string' }, description: 'Filter by source types' },
|
|
29
|
+
entity_types: { type: 'array', items: { type: 'string' }, description: 'Filter by entity types' },
|
|
30
|
+
},
|
|
31
|
+
required: ['query'],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build a search request object. Call .execute() to perform the HTTP call.
|
|
36
|
+
* Exported for testability (allows injecting a mock TacitClient).
|
|
37
|
+
*/
|
|
38
|
+
function buildSearchRequest(client, input) {
|
|
39
|
+
return {
|
|
40
|
+
async execute() {
|
|
41
|
+
const { query, limit, source_types, entity_types } = input;
|
|
42
|
+
try {
|
|
43
|
+
const params = { q: query };
|
|
44
|
+
if (limit !== undefined)
|
|
45
|
+
params['limit'] = limit;
|
|
46
|
+
if (source_types && source_types.length > 0)
|
|
47
|
+
params['source_types'] = source_types.join(',');
|
|
48
|
+
if (entity_types && entity_types.length > 0)
|
|
49
|
+
params['entity_types'] = entity_types.join(',');
|
|
50
|
+
// GET /api/v1/search?q=<term>
|
|
51
|
+
const result = await client.get('/api/v1/search', params);
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return apiErrorResult(err);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function handleSearch(args) {
|
|
63
|
+
const parsed = exports.searchInputSchema.safeParse(args);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return buildSearchRequest((0, client_js_1.getClient)(), parsed.data).execute();
|
|
71
|
+
}
|
|
72
|
+
function apiErrorResult(err) {
|
|
73
|
+
if (err instanceof client_js_1.TacitApiError) {
|
|
74
|
+
return { content: [{ type: 'text', text: `API error ${err.statusCode}: ${err.body}` }], isError: true };
|
|
75
|
+
}
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
78
|
+
}
|