@anteros/core 0.0.41 → 0.0.44
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/database/audit.ts +9 -59
- package/database/collection.ts +3 -1
- package/database/deleteLog.ts +67 -3
- package/database/file.ts +4 -3
- package/database/mongodbadapter.ts +103 -42
- package/database/replication.ts +724 -58
- package/database/ttl.ts +68 -0
- package/database/workflow.ts +466 -240
- package/index.ts +6 -0
- package/lib/agent.ts +1567 -0
- package/lib/agentMemory.ts +689 -0
- package/lib/agents.ts +180 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/auditResult.ts +13 -0
- package/lib/define.ts +43 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +2 -1
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +7 -1
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/mcpClient.ts +128 -0
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +970 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/skills.ts +481 -0
- package/lib/sockets.ts +3 -1
- package/lib/workflow.ts +209 -37
- package/lib/zip.ts +109 -0
- package/package.json +1 -1
- package/server/access.ts +96 -0
- package/server/agents.ts +472 -0
- package/server/api.ts +80 -65
- package/server/boot.ts +11 -0
- package/server/hono.ts +8 -3
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +404 -0
- package/tests/agent.test.ts +1241 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-operations.test.ts +1 -1
- package/tests/audit-redact.test.ts +29 -3
- package/tests/audit-run-action.test.ts +1 -1
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
- package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
- package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
- package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
- package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
- package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
- package/tests/fixtures/attachment/data.csv +2 -0
- package/tests/fixtures/fake-provider.ts +128 -0
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
- package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
- package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
- package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
- package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
- package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
- package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
- package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
- package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
- package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
- package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
- package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
- package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
- package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
- package/tests/hono.test.ts +6 -3
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +4 -2
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +246 -0
- package/tests/replication.test.ts +2 -0
- package/tests/upload.test.ts +44 -12
- package/tests/vars-api.test.ts +1 -1
- package/tests/vars-replication.test.ts +5 -2
- package/tests/workflow-engine.test.ts +340 -0
- package/tests/workflow-retention.test.ts +103 -0
- package/tests/workflow.test.ts +186 -0
- package/types/agent.d.ts +1067 -0
- package/types/config.d.ts +16 -0
- package/types/file.d.ts +7 -7
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +6 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +70 -4
- package/types/route.d.ts +7 -1
- package/types/scripts.d.ts +6 -0
- package/types/service.d.ts +7 -1
- package/types/tenant.d.ts +13 -0
- package/types/vars.d.ts +2 -2
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
package/server/boot.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { syncFileCollections } from '../database/file'
|
|
|
14
14
|
import { syncVars } from '../database/vars'
|
|
15
15
|
import { startReplication, stopReplication } from '../database/replication'
|
|
16
16
|
import { loadLifecycles, runBeforeBoot, runAfterBoot, runOnDestroy } from '../lib/lifecycle'
|
|
17
|
+
import { workflowStats } from '../lib/workflow'
|
|
17
18
|
|
|
18
19
|
import { loadRoutes } from '../lib/routes'
|
|
19
20
|
import { runScripts } from '../lib/scripts'
|
|
@@ -22,6 +23,7 @@ import { loadServices } from '../lib/services'
|
|
|
22
23
|
import { loadSockets } from '../lib/sockets'
|
|
23
24
|
import { syncWorkflows } from '../lib/workflow'
|
|
24
25
|
import { syncMcpTools } from '../lib/mcp'
|
|
26
|
+
import { syncAgents, agentsStats } from '../lib/agents'
|
|
25
27
|
import { loadTenantsMiddlewares } from '../lib/middleware'
|
|
26
28
|
import crypto from 'node:crypto';
|
|
27
29
|
import os from 'node:os';
|
|
@@ -87,6 +89,14 @@ function renderBanner(opts: {
|
|
|
87
89
|
}
|
|
88
90
|
box += `Replication: ${replicationInfo}`.gray.bold + '\n'
|
|
89
91
|
|
|
92
|
+
// Workflows loaded from `{tenant.dir}/workflows/**/*.workflow.ts`
|
|
93
|
+
const wf = workflowStats();
|
|
94
|
+
box += `Workflows: ${wf.total ? `${wf.total}`.green.bold + ` (${wf.tenants.join(', ')})`.gray : 'none'.gray}\n`.gray.bold
|
|
95
|
+
|
|
96
|
+
// Agents loaded from `{tenant.dir}/agents/**/*.agent.ts`
|
|
97
|
+
const ag = agentsStats();
|
|
98
|
+
box += `Agents: ${ag.total ? `${ag.total}`.green.bold + ` (${ag.tenants.join(', ')})`.gray : 'none'.gray}\n`.gray.bold
|
|
99
|
+
|
|
90
100
|
// Only shown when something is disabled — the default banner stays compact
|
|
91
101
|
if (off.length) {
|
|
92
102
|
box += `Mode: ${capabilitiesLabel(caps)}`.gray.bold + ` (${off.join(', ')} off)`.yellow + '\n';
|
|
@@ -144,6 +154,7 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
|
|
|
144
154
|
if (caps.sockets) await loadSockets(); // load websocket handlers
|
|
145
155
|
await syncWorkflows();
|
|
146
156
|
await syncMcpTools(); // load MCP tools per tenant (mcp/**/*.tool.ts)
|
|
157
|
+
await syncAgents(); // load agents per tenant (agents/**/*.agent.ts)
|
|
147
158
|
await loadTenantsMiddlewares();
|
|
148
159
|
await loadRoutes(); // load routes
|
|
149
160
|
}
|
package/server/hono.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { Redis } from "ioredis"
|
|
|
11
11
|
import { cfg } from "./config"
|
|
12
12
|
import { rateLimit, createRedisStore, createMemoryStore, type RateLimitStore } from "./security"
|
|
13
13
|
import { initializeRoutes } from "./routes";
|
|
14
|
+
import { patterns } from "../lib/endpoints";
|
|
14
15
|
import { asyncContextStorage, requestCtxStorage } from "../lib/asyncContextStorage";
|
|
15
16
|
import { initializeApi } from "./api";
|
|
17
|
+
import { initializeAgents } from "./agents";
|
|
16
18
|
import { initializeMcp } from "./mcp";
|
|
17
19
|
import { getGlobalMiddlewares } from "../lib/middleware";
|
|
18
20
|
import { jwt } from "../utils/func";
|
|
@@ -326,8 +328,8 @@ function createApp(): Hono<{ Variables: HonoVariables }> {
|
|
|
326
328
|
keyGenerator: keyByClientIp(''),
|
|
327
329
|
}));
|
|
328
330
|
|
|
329
|
-
// stricter limit for login endpoints
|
|
330
|
-
app.use(
|
|
331
|
+
// stricter limit for login endpoints — the login is a collection action
|
|
332
|
+
app.use(patterns.login, rateLimit({
|
|
331
333
|
windowMs: cfg.server.rateLimit?.login?.windowMs ?? 60_000,
|
|
332
334
|
max: cfg.server.rateLimit?.login?.max ?? 10,
|
|
333
335
|
store,
|
|
@@ -399,7 +401,10 @@ function createApp(): Hono<{ Variables: HonoVariables }> {
|
|
|
399
401
|
// initialize api
|
|
400
402
|
initializeApi(app);
|
|
401
403
|
|
|
402
|
-
// initialize
|
|
404
|
+
// initialize the agents API (/api/:tenant_id/agents/:agent/:action)
|
|
405
|
+
initializeAgents(app);
|
|
406
|
+
|
|
407
|
+
// initialize MCP (tools served at /api/:tenant_id/mcp)
|
|
403
408
|
initializeMcp(app);
|
|
404
409
|
|
|
405
410
|
|
package/server/mcp.ts
CHANGED
|
@@ -10,72 +10,18 @@ import {
|
|
|
10
10
|
ErrorCode,
|
|
11
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
13
|
-
import type Joi from "joi";
|
|
14
13
|
import { cfg } from "./config";
|
|
15
14
|
import { useRest } from "../database/rest";
|
|
15
|
+
import { createAgents } from "../lib/agents";
|
|
16
|
+
import { createApi } from "../lib/api";
|
|
16
17
|
import { logger } from "../utils/logger";
|
|
17
18
|
import type { McpTool, McpResource } from "../types/mcp";
|
|
18
19
|
import type { HonoVariables } from "./env";
|
|
20
|
+
import { joiToJsonSchema } from "../lib/jsonSchema";
|
|
21
|
+
import { patterns } from "../lib/endpoints";
|
|
19
22
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
function joiTypeToJson(type: string): string {
|
|
23
|
-
switch (type) {
|
|
24
|
-
case 'number': return 'number';
|
|
25
|
-
case 'integer': return 'integer';
|
|
26
|
-
case 'boolean': return 'boolean';
|
|
27
|
-
case 'date': return 'string';
|
|
28
|
-
default: return 'string';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Minimal Joi → JSON Schema converter (covers the common field types). */
|
|
33
|
-
function joiToJsonSchema(schema: Joi.Schema, describe = schema.describe() as any): any {
|
|
34
|
-
const node: any = {};
|
|
35
|
-
const type = describe?.type;
|
|
36
|
-
|
|
37
|
-
switch (type) {
|
|
38
|
-
case 'object': {
|
|
39
|
-
node.type = 'object';
|
|
40
|
-
const properties: Record<string, any> = {};
|
|
41
|
-
const required: string[] = [];
|
|
42
|
-
for (const [key, child] of Object.entries(describe.keys ?? {})) {
|
|
43
|
-
properties[key] = joiToJsonSchema(child as any, child);
|
|
44
|
-
if ((child as any)?.flags?.presence === 'required') required.push(key);
|
|
45
|
-
}
|
|
46
|
-
node.properties = properties;
|
|
47
|
-
if (required.length) node.required = required;
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
case 'array': {
|
|
51
|
-
node.type = 'array';
|
|
52
|
-
if (describe.items?.length) node.items = joiToJsonSchema(describe.items[0], describe.items[0]);
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
default: {
|
|
56
|
-
node.type = joiTypeToJson(type);
|
|
57
|
-
if (type === 'date') node.format = 'date-time';
|
|
58
|
-
if (type === 'string') {
|
|
59
|
-
for (const rule of describe.rules ?? []) {
|
|
60
|
-
if (['email', 'uri', 'uuid', 'isoDate', 'ip'].includes(rule.name)) {
|
|
61
|
-
node.format = rule.name;
|
|
62
|
-
}
|
|
63
|
-
if (rule.name === 'pattern' && rule.args?.regex) {
|
|
64
|
-
node.pattern = String(rule.args.regex).replace(/^\/|\/[gimsuy]*$/g, '');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (describe.valids?.length && describe.valids.length <= 50) {
|
|
69
|
-
node.enum = describe.valids;
|
|
70
|
-
}
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (describe?.flags?.presence === 'required') node.description = node.description ?? 'required';
|
|
76
|
-
if (describe?.flags?.description) node.description = describe.flags.description;
|
|
77
|
-
return node;
|
|
78
|
-
}
|
|
23
|
+
// The Joi → JSON Schema converter is shared with the agent runtime — a tool
|
|
24
|
+
// must look identical whether it is exposed over MCP or to a model.
|
|
79
25
|
|
|
80
26
|
// ─── URI template matching (users://{id} → params) ───────────────────────
|
|
81
27
|
|
|
@@ -148,7 +94,13 @@ function buildServer(tenantId: string, tools: McpTool[], resources: McpResource[
|
|
|
148
94
|
|
|
149
95
|
const rest = new useRest({ tenant_id: tenantId });
|
|
150
96
|
try {
|
|
151
|
-
const result = await tool.exec({
|
|
97
|
+
const result = await tool.exec({
|
|
98
|
+
c: undefined as any,
|
|
99
|
+
rest,
|
|
100
|
+
args: value,
|
|
101
|
+
agents: createAgents(tenantId, rest),
|
|
102
|
+
api: createApi(rest),
|
|
103
|
+
});
|
|
152
104
|
const isError = !!(result as any)?.isError;
|
|
153
105
|
logger[isError ? 'warn' : 'info']('MCP tools/call', { method: 'tools/call', tenant: tenantId, tool: req.params.name, args: value, isError, duration: ms(start) });
|
|
154
106
|
if (result && Array.isArray(result.content)) return result;
|
|
@@ -205,6 +157,8 @@ function buildServer(tenantId: string, tools: McpTool[], resources: McpResource[
|
|
|
205
157
|
const result = await matched.resource.read({
|
|
206
158
|
c: undefined as any,
|
|
207
159
|
rest,
|
|
160
|
+
agents: createAgents(tenantId, rest),
|
|
161
|
+
api: createApi(rest),
|
|
208
162
|
params: matched.params,
|
|
209
163
|
uri,
|
|
210
164
|
});
|
|
@@ -221,7 +175,7 @@ function buildServer(tenantId: string, tools: McpTool[], resources: McpResource[
|
|
|
221
175
|
|
|
222
176
|
/**
|
|
223
177
|
* Expose each tenant's MCP tools & resources over the Model Context Protocol
|
|
224
|
-
* at `GET/POST /
|
|
178
|
+
* at `GET/POST /api/:tenant_id/mcp` (Streamable HTTP transport — works with
|
|
225
179
|
* Claude, Cursor, VS Code, and any MCP client).
|
|
226
180
|
*/
|
|
227
181
|
export function initializeMcp(app: Hono<{ Variables: HonoVariables }>) {
|
|
@@ -238,7 +192,7 @@ export function initializeMcp(app: Hono<{ Variables: HonoVariables }>) {
|
|
|
238
192
|
resourcesByTenant.set(resource._tenant_ ?? '', list);
|
|
239
193
|
}
|
|
240
194
|
|
|
241
|
-
app.all(
|
|
195
|
+
app.all(patterns.mcp, async (c: Context<{ Variables: HonoVariables }>) => {
|
|
242
196
|
const tenantId = c.req.param('tenant_id') ?? '';
|
|
243
197
|
const tools = toolsByTenant.get(tenantId) ?? [];
|
|
244
198
|
const resources = resourcesByTenant.get(tenantId) ?? [];
|
package/server/routes.ts
CHANGED
|
@@ -3,10 +3,26 @@ import { cfg } from "./config";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { jwt } from "../utils/func";
|
|
5
5
|
import { useRest } from "../database/rest";
|
|
6
|
+
import { createAgents } from "../lib/agents";
|
|
7
|
+
import { createApi } from "../lib/api";
|
|
6
8
|
import { io } from "./io";
|
|
7
9
|
import { requestCtxStorage } from "../lib/asyncContextStorage";
|
|
8
10
|
import type { HonoVariables } from "./env";
|
|
9
11
|
|
|
12
|
+
/** The context of a route handler — `rest` and `agents` share the same tenant. */
|
|
13
|
+
function routeContext(c: any, tenantId: string) {
|
|
14
|
+
const rest = new useRest({ tenant_id: tenantId });
|
|
15
|
+
return {
|
|
16
|
+
c,
|
|
17
|
+
rest,
|
|
18
|
+
// Tenant-scoped LLM registry, bound to this request's `rest`
|
|
19
|
+
agents: createAgents(tenantId, rest),
|
|
20
|
+
api: createApi(rest),
|
|
21
|
+
jwt: jwt,
|
|
22
|
+
io: io,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
function initializeRoutes(app: Hono<{ Variables: HonoVariables }>) {
|
|
11
27
|
for (let route of cfg.routes ?? []) {
|
|
12
28
|
if (route._prefix_) {
|
|
@@ -24,42 +40,21 @@ function initializeRoutes(app: Hono<{ Variables: HonoVariables }>) {
|
|
|
24
40
|
if (route.method == "GET") {
|
|
25
41
|
app.get(routePath, async (c) => { //
|
|
26
42
|
resolveTenant();
|
|
27
|
-
return route.handler(
|
|
28
|
-
c,
|
|
29
|
-
rest: new useRest({
|
|
30
|
-
tenant_id: route._tenant_,
|
|
31
|
-
}),
|
|
32
|
-
jwt: jwt,
|
|
33
|
-
io: io,
|
|
34
|
-
});
|
|
43
|
+
return route.handler(routeContext(c, route._tenant_!));
|
|
35
44
|
});
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
if (route.method == 'POST') {
|
|
39
48
|
app.post(routePath, async (c) => {
|
|
40
49
|
resolveTenant();
|
|
41
|
-
return route.handler(
|
|
42
|
-
c,
|
|
43
|
-
rest: new useRest({
|
|
44
|
-
tenant_id: route._tenant_,
|
|
45
|
-
}),
|
|
46
|
-
jwt: jwt,
|
|
47
|
-
io: io,
|
|
48
|
-
});
|
|
50
|
+
return route.handler(routeContext(c, route._tenant_!));
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
if (route.method == "PUT") {
|
|
53
55
|
app.put(routePath, async (c) => { //
|
|
54
56
|
resolveTenant();
|
|
55
|
-
return route.handler(
|
|
56
|
-
c,
|
|
57
|
-
rest: new useRest({
|
|
58
|
-
tenant_id: route._tenant_,
|
|
59
|
-
}),
|
|
60
|
-
jwt: jwt,
|
|
61
|
-
io: io,
|
|
62
|
-
});
|
|
57
|
+
return route.handler(routeContext(c, route._tenant_!));
|
|
63
58
|
});
|
|
64
59
|
}
|
|
65
60
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP **client** — an agent consuming a remote server's tools.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint under test is the framework's own (`/api/:tenant/mcp`, served by
|
|
5
|
+
* `@hono/mcp`): the SDK client and our server are two independent implementations of
|
|
6
|
+
* the protocol, which is exactly what makes the round trip worth testing end to end —
|
|
7
|
+
* list, call, and back into the conversation as if the tool were local.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
10
|
+
import { Agent, define, v } from "../index";
|
|
11
|
+
import { cfg, formatConfig } from "../server/config";
|
|
12
|
+
import { syncTenants } from "../database/tenant";
|
|
13
|
+
import { syncMcpTools } from "../lib/mcp";
|
|
14
|
+
import { closeMcpClients } from "../lib/mcpClient";
|
|
15
|
+
import { createApp } from "../server/hono";
|
|
16
|
+
import { at, fakeProvider, openaiText, openaiTool, request, type Fake } from "./fixtures/fake-provider";
|
|
17
|
+
|
|
18
|
+
const TENANT = "mcp-client";
|
|
19
|
+
const DIR = "packages/core/tests/fixtures/mcp-tenant";
|
|
20
|
+
|
|
21
|
+
let server: any;
|
|
22
|
+
let mcpUrl: string;
|
|
23
|
+
const servers: Fake[] = [];
|
|
24
|
+
|
|
25
|
+
const track = (fake: Fake): Fake => (servers.push(fake), fake);
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
formatConfig({
|
|
29
|
+
server: { port: 4000 },
|
|
30
|
+
tenants: [{ id: TENANT, dir: DIR, database: { uri: "mongodb://localhost:27017/none" } }],
|
|
31
|
+
});
|
|
32
|
+
await syncTenants();
|
|
33
|
+
await syncMcpTools();
|
|
34
|
+
|
|
35
|
+
server = Bun.serve({ port: 0, fetch: createApp().fetch });
|
|
36
|
+
mcpUrl = `${server.url.href.replace(/\/$/, "")}/api/${TENANT}/mcp`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
for (const fake of servers) fake.stop();
|
|
41
|
+
await closeMcpClients();
|
|
42
|
+
try { server?.stop(true); } catch (_) {}
|
|
43
|
+
cfg.agents = [];
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function makeAgent(provider: Fake, overrides: Record<string, any> = {}) {
|
|
47
|
+
return new Agent({
|
|
48
|
+
id: "test",
|
|
49
|
+
description: "A test agent.",
|
|
50
|
+
instructions: "You are a test agent.",
|
|
51
|
+
provider: { model: "test-model", apiKey: "test-key", baseUrl: provider.url, options: { retries: 0 } },
|
|
52
|
+
mcp: [mcpUrl],
|
|
53
|
+
...overrides,
|
|
54
|
+
} as any);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("agent mcp — a remote server's tools", () => {
|
|
58
|
+
it("lists them as tools, calls one, and feeds the result back", async () => {
|
|
59
|
+
const provider = track(fakeProvider((_body, _req, hits) =>
|
|
60
|
+
hits === 1 ? openaiTool("echo", { msg: "hello mcp", times: 1 }) : openaiText("done")));
|
|
61
|
+
const agent = makeAgent(provider);
|
|
62
|
+
|
|
63
|
+
const result = await agent.generate("echo something");
|
|
64
|
+
|
|
65
|
+
// The remote tools are offered like any other
|
|
66
|
+
const names = request(provider, 0).body.tools.map((tool: any) => tool.function.name);
|
|
67
|
+
expect(names).toContain("echo");
|
|
68
|
+
expect(names).toContain("badge");
|
|
69
|
+
|
|
70
|
+
// …and the call really crossed the wire: the answer is the server's
|
|
71
|
+
const entry = at(result.toolResults, 0);
|
|
72
|
+
expect(entry.error).toBeUndefined();
|
|
73
|
+
expect(entry.result.content[0].text).toContain("hello mcp");
|
|
74
|
+
// What the model saw is the flattened text, not the protocol envelope
|
|
75
|
+
const toolMessage = result.messages.find((message) => message.role === "tool");
|
|
76
|
+
expect(toolMessage?.content).toContain("hello mcp");
|
|
77
|
+
expect(result.text).toBe("done");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("lets a declared tool win over a remote one of the same name", async () => {
|
|
81
|
+
const provider = track(fakeProvider(() => openaiText("done")));
|
|
82
|
+
const local = define.Tool({
|
|
83
|
+
id: "echo",
|
|
84
|
+
description: "The tenant's own echo.",
|
|
85
|
+
inputSchema: v.object({}),
|
|
86
|
+
execute: async () => "local",
|
|
87
|
+
});
|
|
88
|
+
const agent = makeAgent(provider, { tools: { echo: local } });
|
|
89
|
+
|
|
90
|
+
await agent.generate("hi");
|
|
91
|
+
|
|
92
|
+
const offered = request(provider, 0).body.tools.filter((tool: any) => tool.function.name === "echo");
|
|
93
|
+
expect(offered).toHaveLength(1);
|
|
94
|
+
expect(offered[0].function.description).toBe("The tenant's own echo.");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("contacts nothing with `tools: false`", async () => {
|
|
98
|
+
const provider = track(fakeProvider(() => openaiText("direct")));
|
|
99
|
+
const agent = makeAgent(provider);
|
|
100
|
+
|
|
101
|
+
await agent.generate("hi", { tools: false });
|
|
102
|
+
|
|
103
|
+
expect(request(provider, 0).body.tools).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("runs anyway when the server is unreachable", async () => {
|
|
107
|
+
const provider = track(fakeProvider(() => openaiText("still here")));
|
|
108
|
+
const agent = makeAgent(provider, { mcp: ["http://127.0.0.1:9/mcp"] });
|
|
109
|
+
|
|
110
|
+
const result = await agent.generate("hi");
|
|
111
|
+
|
|
112
|
+
// A remote that is down costs its tools, not the run
|
|
113
|
+
expect(result.text).toBe("still here");
|
|
114
|
+
expect(request(provider, 0).body.tools).toBeUndefined();
|
|
115
|
+
}, 20_000);
|
|
116
|
+
});
|