agent-tool-forge 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.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent-registry.js +170 -0
- package/lib/api-client.js +792 -0
- package/lib/api-loader.js +260 -0
- package/lib/auth.d.ts +25 -0
- package/lib/auth.js +158 -0
- package/lib/checks/check-adapter.js +172 -0
- package/lib/checks/compose.js +42 -0
- package/lib/checks/content-match.js +14 -0
- package/lib/checks/cost-budget.js +11 -0
- package/lib/checks/index.js +18 -0
- package/lib/checks/json-valid.js +15 -0
- package/lib/checks/latency.js +11 -0
- package/lib/checks/length-bounds.js +17 -0
- package/lib/checks/negative-match.js +14 -0
- package/lib/checks/no-hallucinated-numbers.js +63 -0
- package/lib/checks/non-empty.js +34 -0
- package/lib/checks/regex-match.js +12 -0
- package/lib/checks/run-checks.js +84 -0
- package/lib/checks/schema-match.js +26 -0
- package/lib/checks/tool-call-count.js +16 -0
- package/lib/checks/tool-selection.js +34 -0
- package/lib/checks/types.js +45 -0
- package/lib/comparison/compare.js +86 -0
- package/lib/comparison/format.js +104 -0
- package/lib/comparison/index.js +6 -0
- package/lib/comparison/statistics.js +59 -0
- package/lib/comparison/types.js +41 -0
- package/lib/config-schema.js +200 -0
- package/lib/config.d.ts +66 -0
- package/lib/conversation-store.d.ts +77 -0
- package/lib/conversation-store.js +443 -0
- package/lib/db.d.ts +6 -0
- package/lib/db.js +1112 -0
- package/lib/dep-check.js +99 -0
- package/lib/drift-background.js +61 -0
- package/lib/drift-monitor.js +187 -0
- package/lib/eval-runner.js +566 -0
- package/lib/fixtures/fixture-store.js +161 -0
- package/lib/fixtures/index.js +11 -0
- package/lib/forge-engine.js +982 -0
- package/lib/forge-eval-generator.js +417 -0
- package/lib/forge-file-writer.js +386 -0
- package/lib/forge-service-client.js +190 -0
- package/lib/forge-service.d.ts +4 -0
- package/lib/forge-service.js +655 -0
- package/lib/forge-verifier-generator.js +271 -0
- package/lib/handlers/admin.js +151 -0
- package/lib/handlers/agents.js +229 -0
- package/lib/handlers/chat-resume.js +334 -0
- package/lib/handlers/chat-sync.js +320 -0
- package/lib/handlers/chat.js +320 -0
- package/lib/handlers/conversations.js +92 -0
- package/lib/handlers/preferences.js +88 -0
- package/lib/handlers/tools-list.js +58 -0
- package/lib/hitl-engine.d.ts +60 -0
- package/lib/hitl-engine.js +261 -0
- package/lib/http-utils.js +92 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +141 -0
- package/lib/init.js +636 -0
- package/lib/manual-entry.js +59 -0
- package/lib/mcp-server.js +252 -0
- package/lib/output-groups.js +54 -0
- package/lib/postgres-store.d.ts +31 -0
- package/lib/postgres-store.js +465 -0
- package/lib/preference-store.d.ts +47 -0
- package/lib/preference-store.js +79 -0
- package/lib/prompt-store.d.ts +42 -0
- package/lib/prompt-store.js +60 -0
- package/lib/rate-limiter.d.ts +30 -0
- package/lib/rate-limiter.js +104 -0
- package/lib/react-engine.d.ts +110 -0
- package/lib/react-engine.js +337 -0
- package/lib/runner/cli.js +156 -0
- package/lib/runner/cost-estimator.js +71 -0
- package/lib/runner/gate.js +46 -0
- package/lib/runner/index.js +165 -0
- package/lib/sidecar.d.ts +83 -0
- package/lib/sidecar.js +161 -0
- package/lib/sse.d.ts +15 -0
- package/lib/sse.js +30 -0
- package/lib/tools-scanner.js +91 -0
- package/lib/tui.js +253 -0
- package/lib/verifier-report.js +78 -0
- package/lib/verifier-runner.js +338 -0
- package/lib/verifier-scanner.js +70 -0
- package/lib/verifier-worker-pool.js +196 -0
- package/lib/views/chat.js +340 -0
- package/lib/views/endpoints.js +203 -0
- package/lib/views/eval-run.js +206 -0
- package/lib/views/forge-agent.js +538 -0
- package/lib/views/forge.js +410 -0
- package/lib/views/main-menu.js +275 -0
- package/lib/views/mediation.js +381 -0
- package/lib/views/model-compare.js +430 -0
- package/lib/views/model-comparison.js +333 -0
- package/lib/views/onboarding.js +470 -0
- package/lib/views/performance.js +237 -0
- package/lib/views/run-evals.js +205 -0
- package/lib/views/settings.js +829 -0
- package/lib/views/tools-evals.js +514 -0
- package/lib/views/verifier-coverage.js +617 -0
- package/lib/workers/verifier-worker.js +52 -0
- package/package.json +123 -0
- package/widget/forge-chat.js +789 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server — Creates an MCP Server instance that proxies tool calls
|
|
3
|
+
* to internal API endpoints defined in tool_registry.
|
|
4
|
+
*
|
|
5
|
+
* Usage: const server = createMcpServer(db, config);
|
|
6
|
+
* then: const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
7
|
+
* await server.connect(transport);
|
|
8
|
+
* await transport.handleRequest(req, res, parsedBody);
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server';
|
|
12
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { getAllToolRegistry, insertMcpCallLog } from './db.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Safe JSON.parse — returns null on failure.
|
|
17
|
+
* @param {string} str
|
|
18
|
+
* @returns {object|null}
|
|
19
|
+
*/
|
|
20
|
+
function safeParseJson(str) {
|
|
21
|
+
try { return JSON.parse(str); } catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Call a tool's mcpRouting endpoint with the provided arguments.
|
|
26
|
+
* Builds URL from config.api.baseUrl + tool's mcpRouting.endpoint.
|
|
27
|
+
* Maps tool arguments to path params, query params, or body via paramMap.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} spec - Parsed tool spec with mcpRouting
|
|
30
|
+
* @param {object} args - Tool call arguments
|
|
31
|
+
* @param {object} config - forge config with api.baseUrl
|
|
32
|
+
* @param {string|null} [userJwt] - User JWT to forward as Authorization header
|
|
33
|
+
* @returns {Promise<{ status: number; body: object; error: string|null }>}
|
|
34
|
+
*/
|
|
35
|
+
async function callToolEndpoint(spec, args, config, userJwt = null) {
|
|
36
|
+
const baseUrl = (config.api?.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
|
|
37
|
+
const routing = spec.mcpRouting || {};
|
|
38
|
+
const path = routing.endpoint || '/';
|
|
39
|
+
const method = (routing.method || 'GET').toUpperCase();
|
|
40
|
+
const paramMap = routing.paramMap || {};
|
|
41
|
+
|
|
42
|
+
// Build URL with path params substituted; collect query and body params
|
|
43
|
+
let url = baseUrl + path;
|
|
44
|
+
const queryParams = new URLSearchParams();
|
|
45
|
+
const bodyObj = {};
|
|
46
|
+
|
|
47
|
+
// Validate property names before use to prevent prototype pollution
|
|
48
|
+
const SAFE_PROP_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
49
|
+
|
|
50
|
+
for (const [toolParam, mapping] of Object.entries(paramMap)) {
|
|
51
|
+
const val = args[toolParam];
|
|
52
|
+
if (val === undefined) continue;
|
|
53
|
+
if (mapping.path) {
|
|
54
|
+
// Validate path param name before substituting into URL
|
|
55
|
+
if (SAFE_PROP_NAME.test(mapping.path)) {
|
|
56
|
+
url = url.replace(`{${mapping.path}}`, encodeURIComponent(String(val)));
|
|
57
|
+
}
|
|
58
|
+
// silently skip invalid path param names
|
|
59
|
+
} else if (mapping.query) {
|
|
60
|
+
if (!SAFE_PROP_NAME.test(mapping.query)) {
|
|
61
|
+
throw new Error(`Unsafe mapping.query key: ${mapping.query}`);
|
|
62
|
+
}
|
|
63
|
+
queryParams.set(mapping.query, String(val));
|
|
64
|
+
} else if (mapping.body) {
|
|
65
|
+
// Validate body property name to prevent prototype pollution
|
|
66
|
+
if (SAFE_PROP_NAME.test(mapping.body)) {
|
|
67
|
+
bodyObj[mapping.body] = val;
|
|
68
|
+
}
|
|
69
|
+
// silently skip invalid property names
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if ([...queryParams].length > 0) url += '?' + queryParams.toString();
|
|
74
|
+
|
|
75
|
+
const fetchOpts = {
|
|
76
|
+
method,
|
|
77
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
78
|
+
signal: AbortSignal.timeout(30_000)
|
|
79
|
+
};
|
|
80
|
+
// Forward user JWT if present — allows the app's internal API to validate the user
|
|
81
|
+
if (userJwt) {
|
|
82
|
+
fetchOpts.headers['Authorization'] = `Bearer ${userJwt}`;
|
|
83
|
+
}
|
|
84
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && Object.keys(bodyObj).length > 0) {
|
|
85
|
+
fetchOpts.body = JSON.stringify(bodyObj);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const res = await fetch(url, fetchOpts);
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
let body;
|
|
91
|
+
try { body = JSON.parse(text); } catch { body = { text }; }
|
|
92
|
+
return {
|
|
93
|
+
status: res.status,
|
|
94
|
+
body,
|
|
95
|
+
error: res.ok ? null : `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create an MCP Server that exposes promoted tools from tool_registry.
|
|
101
|
+
*
|
|
102
|
+
* @param {import('better-sqlite3').Database} db
|
|
103
|
+
* @param {object} config - forge.config.json contents
|
|
104
|
+
* @returns {import('@modelcontextprotocol/sdk/server').Server}
|
|
105
|
+
*/
|
|
106
|
+
export function createMcpServer(db, config, sidecarCtx = null) {
|
|
107
|
+
const server = new Server(
|
|
108
|
+
{ name: 'forge-mcp-server', version: '1.0.0' },
|
|
109
|
+
{ capabilities: { tools: {} } }
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── tools/list ──────────────────────────────────────────────────────────
|
|
113
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
114
|
+
const rows = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
|
|
115
|
+
const tools = [];
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
const spec = safeParseJson(row.spec_json);
|
|
118
|
+
if (!spec) {
|
|
119
|
+
console.error(`[mcp-server] Skipping tool "${row.tool_name}": malformed spec_json`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const schema = spec.schema || {};
|
|
123
|
+
const properties = {};
|
|
124
|
+
const required = [];
|
|
125
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
126
|
+
properties[k] = {
|
|
127
|
+
type: v.type || 'string',
|
|
128
|
+
description: v.description || k
|
|
129
|
+
};
|
|
130
|
+
if (!v.optional) required.push(k);
|
|
131
|
+
}
|
|
132
|
+
tools.push({
|
|
133
|
+
name: spec.name || row.tool_name,
|
|
134
|
+
description: spec.description || '',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties,
|
|
138
|
+
...(required.length ? { required } : {})
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { tools };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── tools/call ───────────────────────────────────────────────────────────
|
|
146
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
147
|
+
const { name, arguments: args = {} } = request.params;
|
|
148
|
+
const rows = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
|
|
149
|
+
const row = rows.find(r => {
|
|
150
|
+
const s = safeParseJson(r.spec_json);
|
|
151
|
+
return (s?.name || r.tool_name) === name;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!row) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: 'text', text: `Tool "${name}" not found or not promoted` }],
|
|
157
|
+
isError: true
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const spec = safeParseJson(row.spec_json);
|
|
162
|
+
if (!spec?.mcpRouting?.endpoint) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: `Tool "${name}" has no mcpRouting configured` }],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const start = Date.now();
|
|
170
|
+
let result;
|
|
171
|
+
try {
|
|
172
|
+
// MCP protocol doesn't carry user JWTs; tool calls made via MCP are
|
|
173
|
+
// service-level. Pass null explicitly so it's clear this is intentional,
|
|
174
|
+
// not an accidental omission. A future transport that carries identity
|
|
175
|
+
// should extract it from sidecarCtx.session and pass it here.
|
|
176
|
+
const userJwt = null; // MCP protocol doesn't carry user JWT
|
|
177
|
+
result = await callToolEndpoint(spec, args, config, userJwt);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const latency_ms = Date.now() - start;
|
|
180
|
+
try {
|
|
181
|
+
insertMcpCallLog(db, {
|
|
182
|
+
tool_name: name,
|
|
183
|
+
input_json: JSON.stringify(args),
|
|
184
|
+
status_code: 0,
|
|
185
|
+
latency_ms,
|
|
186
|
+
error: err.message
|
|
187
|
+
});
|
|
188
|
+
} catch (logErr) {
|
|
189
|
+
console.error('[mcp-server] Failed to log call error:', logErr.message);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: `Connection error: ${err.message}` }],
|
|
193
|
+
isError: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const latency_ms = Date.now() - start;
|
|
198
|
+
try {
|
|
199
|
+
insertMcpCallLog(db, {
|
|
200
|
+
tool_name: name,
|
|
201
|
+
input_json: JSON.stringify(args),
|
|
202
|
+
output_json: JSON.stringify(result.body),
|
|
203
|
+
status_code: result.status,
|
|
204
|
+
latency_ms,
|
|
205
|
+
error: result.error || null
|
|
206
|
+
});
|
|
207
|
+
} catch (logErr) {
|
|
208
|
+
console.error('[mcp-server] Failed to log call:', logErr.message);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (result.error) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text: result.error }],
|
|
214
|
+
isError: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Run verifiers if sidecar context is available
|
|
219
|
+
if (sidecarCtx?.verifierRunner) {
|
|
220
|
+
try {
|
|
221
|
+
await sidecarCtx.verifierRunner.loadFromDb(db);
|
|
222
|
+
const vResult = await sidecarCtx.verifierRunner.verify(name, args, result);
|
|
223
|
+
if (vResult.outcome === 'block') {
|
|
224
|
+
sidecarCtx.verifierRunner.logResult('mcp', name, vResult);
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: 'text', text: `Tool blocked by verifier "${vResult.verifierName}": ${vResult.message}` }],
|
|
227
|
+
isError: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (vResult.outcome === 'warn') {
|
|
231
|
+
sidecarCtx.verifierRunner.logResult('mcp', name, vResult);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: 'text', text: `Warning from verifier "${vResult.verifierName}": ${vResult.message}` },
|
|
235
|
+
{ type: 'text', text: JSON.stringify(result.body, null, 2) }
|
|
236
|
+
],
|
|
237
|
+
structuredContent: result.body,
|
|
238
|
+
isError: false
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
} catch { /* verifier failure is non-fatal */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: 'text', text: JSON.stringify(result.body, null, 2) }],
|
|
246
|
+
structuredContent: result.body,
|
|
247
|
+
isError: false
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return server;
|
|
252
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Groups — Infer which verifiers apply to which tools.
|
|
3
|
+
* Maps tool descriptions/tags to output groups, then to suggested verifiers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const OUTPUT_GROUP_KEYWORDS = {
|
|
7
|
+
holdings: ['holdings', 'positions', 'allocation', 'portfolio'],
|
|
8
|
+
dividends: ['dividends', 'income', 'yield', 'dividend'],
|
|
9
|
+
performance: ['performance', 'p&l', 'returns', 'net worth'],
|
|
10
|
+
transactions: ['transactions', 'orders', 'trades'],
|
|
11
|
+
quotes: ['quotes', 'prices', 'market data', 'market_data'],
|
|
12
|
+
weather: ['weather', 'temperature', 'humidity', 'conditions'],
|
|
13
|
+
forecast: ['forecast', 'prediction', 'outlook', 'precipitation']
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const VERIFIER_GROUPS = {
|
|
17
|
+
source_attribution: ['*'],
|
|
18
|
+
concentration_risk: ['holdings'],
|
|
19
|
+
stale_data: ['holdings', 'performance', 'quotes', 'dividends', 'weather', 'forecast']
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Infer output groups for a tool from its description and tags.
|
|
24
|
+
* @param {{ description?: string; tags?: string[]; name?: string }} tool
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
export function inferOutputGroups(tool) {
|
|
28
|
+
const text = [
|
|
29
|
+
tool.description || '',
|
|
30
|
+
(tool.tags || []).join(' '),
|
|
31
|
+
tool.name || ''
|
|
32
|
+
]
|
|
33
|
+
.join(' ')
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
const groups = [];
|
|
36
|
+
for (const [group, keywords] of Object.entries(OUTPUT_GROUP_KEYWORDS)) {
|
|
37
|
+
if (keywords.some((k) => text.includes(k))) groups.push(group);
|
|
38
|
+
}
|
|
39
|
+
return groups.length > 0 ? groups : ['unknown'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get verifiers that would cover the given output groups.
|
|
44
|
+
* @param {string[]} outputGroups
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
export function getVerifiersForGroups(outputGroups) {
|
|
48
|
+
const verifiers = new Set();
|
|
49
|
+
for (const [verifier, groups] of Object.entries(VERIFIER_GROUPS)) {
|
|
50
|
+
if (groups.includes('*')) verifiers.add(verifier);
|
|
51
|
+
else if (groups.some((g) => outputGroups.includes(g))) verifiers.add(verifier);
|
|
52
|
+
}
|
|
53
|
+
return Array.from(verifiers);
|
|
54
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres-backed storage adapter for horizontal scaling.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the SQLite query function signatures from `db.js` but uses a `pg` Pool.
|
|
5
|
+
* Only loaded when `conversation.store === 'postgres'` (or `database.type === 'postgres'`) in config.
|
|
6
|
+
*
|
|
7
|
+
* Requires: `npm install pg`
|
|
8
|
+
*/
|
|
9
|
+
export class PostgresStore {
|
|
10
|
+
constructor(pgConfig: { connectionString: string });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Connect to Postgres and run schema migrations.
|
|
14
|
+
* Must be called before any other method.
|
|
15
|
+
*/
|
|
16
|
+
connect(): Promise<this>;
|
|
17
|
+
|
|
18
|
+
/** Close the connection pool. */
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
|
|
21
|
+
// ── Prompt versions ───────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
getActivePrompt(): Promise<object | null>;
|
|
24
|
+
insertPromptVersion(row: { version: string; content: string; notes?: string | null }): Promise<number | null>;
|
|
25
|
+
activatePromptVersion(id: number): Promise<void>;
|
|
26
|
+
|
|
27
|
+
// ── User preferences ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
getUserPreferences(userId: string): Promise<{ model: string | null; hitl_level: string | null } | null>;
|
|
30
|
+
upsertUserPreferences(userId: string, prefs: { model?: string; hitlLevel?: string }): Promise<void>;
|
|
31
|
+
}
|