@agentforge-io/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/billing/billing-adapter.interface.d.ts +41 -0
- package/dist/adapters/billing/billing-adapter.interface.js +5 -0
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +30 -0
- package/dist/adapters/billing/stripe/stripe.adapter.js +122 -0
- package/dist/adapters/email/email-adapter.interface.d.ts +25 -0
- package/dist/adapters/email/email-adapter.interface.js +6 -0
- package/dist/adapters/email/noop.adapter.d.ts +10 -0
- package/dist/adapters/email/noop.adapter.js +15 -0
- package/dist/adapters/email/resend.adapter.d.ts +8 -0
- package/dist/adapters/email/resend.adapter.js +39 -0
- package/dist/adapters/job-queue/in-memory.d.ts +43 -0
- package/dist/adapters/job-queue/in-memory.js +154 -0
- package/dist/adapters/job-queue/job-queue.types.d.ts +76 -0
- package/dist/adapters/job-queue/job-queue.types.js +5 -0
- package/dist/adapters/prepared-stream/prepared-stream.types.d.ts +23 -0
- package/dist/adapters/prepared-stream/prepared-stream.types.js +5 -0
- package/dist/adapters/rate-limiter/in-memory.d.ts +19 -0
- package/dist/adapters/rate-limiter/in-memory.js +63 -0
- package/dist/adapters/rate-limiter/rate-limiter.types.d.ts +42 -0
- package/dist/adapters/rate-limiter/rate-limiter.types.js +5 -0
- package/dist/adapters/rate-limiter/redis.d.ts +31 -0
- package/dist/adapters/rate-limiter/redis.js +47 -0
- package/dist/adapters/upload/noop.adapter.d.ts +9 -0
- package/dist/adapters/upload/noop.adapter.js +14 -0
- package/dist/adapters/upload/s3.adapter.d.ts +38 -0
- package/dist/adapters/upload/s3.adapter.js +69 -0
- package/dist/adapters/upload/upload-adapter.interface.d.ts +37 -0
- package/dist/adapters/upload/upload-adapter.interface.js +15 -0
- package/dist/ai/index.d.ts +15 -0
- package/dist/ai/index.js +43 -0
- package/dist/billing/index.d.ts +12 -0
- package/dist/billing/index.js +28 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/domain/agent.d.ts +59 -0
- package/dist/domain/agent.js +2 -0
- package/dist/domain/api-key.d.ts +28 -0
- package/dist/domain/api-key.js +2 -0
- package/dist/domain/auth-identity.d.ts +10 -0
- package/dist/domain/auth-identity.js +2 -0
- package/dist/domain/chat-token.d.ts +39 -0
- package/dist/domain/chat-token.js +2 -0
- package/dist/domain/connector-auth.d.ts +42 -0
- package/dist/domain/connector-auth.js +2 -0
- package/dist/domain/connector.d.ts +52 -0
- package/dist/domain/connector.js +2 -0
- package/dist/domain/conversation.d.ts +26 -0
- package/dist/domain/conversation.js +2 -0
- package/dist/domain/email-token.d.ts +11 -0
- package/dist/domain/email-token.js +2 -0
- package/dist/domain/external-user.d.ts +23 -0
- package/dist/domain/external-user.js +2 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.js +24 -0
- package/dist/domain/mcp-server.d.ts +33 -0
- package/dist/domain/mcp-server.js +2 -0
- package/dist/domain/plan.d.ts +20 -0
- package/dist/domain/plan.js +2 -0
- package/dist/domain/platform-secret.d.ts +24 -0
- package/dist/domain/platform-secret.js +8 -0
- package/dist/domain/refresh-token.d.ts +15 -0
- package/dist/domain/refresh-token.js +2 -0
- package/dist/domain/subscription.d.ts +21 -0
- package/dist/domain/subscription.js +2 -0
- package/dist/domain/tenant.d.ts +21 -0
- package/dist/domain/tenant.js +2 -0
- package/dist/domain/usage-record.d.ts +15 -0
- package/dist/domain/usage-record.js +2 -0
- package/dist/domain/user.d.ts +43 -0
- package/dist/domain/user.js +2 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +59 -0
- package/dist/repositories/in-memory.d.ts +30 -0
- package/dist/repositories/in-memory.js +82 -0
- package/dist/repositories/index.d.ts +67 -0
- package/dist/repositories/index.js +16 -0
- package/dist/services/agent-config.service.d.ts +45 -0
- package/dist/services/agent-config.service.js +114 -0
- package/dist/services/agent-job.worker.d.ts +32 -0
- package/dist/services/agent-job.worker.js +97 -0
- package/dist/services/agent-runner.service.d.ts +35 -0
- package/dist/services/agent-runner.service.js +224 -0
- package/dist/services/agent.service.d.ts +171 -0
- package/dist/services/agent.service.js +329 -0
- package/dist/services/api-key.service.d.ts +41 -0
- package/dist/services/api-key.service.js +80 -0
- package/dist/services/auth.service.d.ts +133 -0
- package/dist/services/auth.service.js +411 -0
- package/dist/services/billing.service.d.ts +67 -0
- package/dist/services/billing.service.js +254 -0
- package/dist/services/chat-token.service.d.ts +29 -0
- package/dist/services/chat-token.service.js +113 -0
- package/dist/services/connector-registry.service.d.ts +156 -0
- package/dist/services/connector-registry.service.js +278 -0
- package/dist/services/conversation.service.d.ts +47 -0
- package/dist/services/conversation.service.js +101 -0
- package/dist/services/email-templates.d.ts +18 -0
- package/dist/services/email-templates.js +39 -0
- package/dist/services/email.service.d.ts +26 -0
- package/dist/services/email.service.js +42 -0
- package/dist/services/errors.d.ts +7 -0
- package/dist/services/errors.js +27 -0
- package/dist/services/in-memory-prepared-stream.store.d.ts +13 -0
- package/dist/services/in-memory-prepared-stream.store.js +35 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +40 -0
- package/dist/services/mcp-client.service.d.ts +64 -0
- package/dist/services/mcp-client.service.js +157 -0
- package/dist/services/mcp-server.service.d.ts +44 -0
- package/dist/services/mcp-server.service.js +147 -0
- package/dist/services/oauth.service.d.ts +73 -0
- package/dist/services/oauth.service.js +174 -0
- package/dist/services/oauth2.service.d.ts +57 -0
- package/dist/services/oauth2.service.js +82 -0
- package/dist/services/orchestrator.service.d.ts +45 -0
- package/dist/services/orchestrator.service.js +180 -0
- package/dist/services/plan.service.d.ts +54 -0
- package/dist/services/plan.service.js +120 -0
- package/dist/services/prepared-stream.service.d.ts +23 -0
- package/dist/services/prepared-stream.service.js +43 -0
- package/dist/services/refresh-token.service.d.ts +38 -0
- package/dist/services/refresh-token.service.js +73 -0
- package/dist/services/secrets/crypto.d.ts +37 -0
- package/dist/services/secrets/crypto.js +110 -0
- package/dist/services/secrets/known-keys.d.ts +38 -0
- package/dist/services/secrets/known-keys.js +50 -0
- package/dist/services/secrets.service.d.ts +91 -0
- package/dist/services/secrets.service.js +193 -0
- package/dist/services/tenant-billing.service.d.ts +121 -0
- package/dist/services/tenant-billing.service.js +290 -0
- package/dist/services/tenant.service.d.ts +54 -0
- package/dist/services/tenant.service.js +96 -0
- package/dist/services/tool-registry.service.d.ts +42 -0
- package/dist/services/tool-registry.service.js +101 -0
- package/dist/services/upload.service.d.ts +37 -0
- package/dist/services/upload.service.js +84 -0
- package/dist/services/usage.service.d.ts +34 -0
- package/dist/services/usage.service.js +108 -0
- package/dist/types/agent.types.d.ts +160 -0
- package/dist/types/agent.types.js +2 -0
- package/dist/types/billing.types.d.ts +82 -0
- package/dist/types/billing.types.js +3 -0
- package/dist/types/config.types.d.ts +127 -0
- package/dist/types/config.types.js +9 -0
- package/dist/types/hooks.d.ts +85 -0
- package/dist/types/hooks.js +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +19 -0
- package/package.json +36 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryAuthorizeStateStore = exports.ConnectorError = exports.ConnectorRegistryService = exports.OAuth2Service = exports.McpServerError = exports.McpServerService = exports.McpClientError = exports.McpClientService = exports.ChatTokenError = exports.ChatTokenService = exports.AgentJobWorker = exports.OrchestratorError = exports.OrchestratorService = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.InMemoryPreparedStreamStore = exports.PreparedStreamError = exports.PreparedStreamService = exports.AgentRunnerService = exports.ToolRegistryService = void 0;
|
|
4
|
+
// Public service surface of @agentforge-io/core. AI runtime only — auth,
|
|
5
|
+
// identity, billing, infra, etc. have all moved to the host server.
|
|
6
|
+
var tool_registry_service_1 = require("./tool-registry.service");
|
|
7
|
+
Object.defineProperty(exports, "ToolRegistryService", { enumerable: true, get: function () { return tool_registry_service_1.ToolRegistryService; } });
|
|
8
|
+
var agent_runner_service_1 = require("./agent-runner.service");
|
|
9
|
+
Object.defineProperty(exports, "AgentRunnerService", { enumerable: true, get: function () { return agent_runner_service_1.AgentRunnerService; } });
|
|
10
|
+
var prepared_stream_service_1 = require("./prepared-stream.service");
|
|
11
|
+
Object.defineProperty(exports, "PreparedStreamService", { enumerable: true, get: function () { return prepared_stream_service_1.PreparedStreamService; } });
|
|
12
|
+
Object.defineProperty(exports, "PreparedStreamError", { enumerable: true, get: function () { return prepared_stream_service_1.PreparedStreamError; } });
|
|
13
|
+
var in_memory_prepared_stream_store_1 = require("./in-memory-prepared-stream.store");
|
|
14
|
+
Object.defineProperty(exports, "InMemoryPreparedStreamStore", { enumerable: true, get: function () { return in_memory_prepared_stream_store_1.InMemoryPreparedStreamStore; } });
|
|
15
|
+
var conversation_service_1 = require("./conversation.service");
|
|
16
|
+
Object.defineProperty(exports, "ConversationService", { enumerable: true, get: function () { return conversation_service_1.ConversationService; } });
|
|
17
|
+
Object.defineProperty(exports, "ConversationNotFoundError", { enumerable: true, get: function () { return conversation_service_1.ConversationNotFoundError; } });
|
|
18
|
+
var agent_service_1 = require("./agent.service");
|
|
19
|
+
Object.defineProperty(exports, "AgentService", { enumerable: true, get: function () { return agent_service_1.AgentService; } });
|
|
20
|
+
Object.defineProperty(exports, "AgentForbiddenError", { enumerable: true, get: function () { return agent_service_1.AgentForbiddenError; } });
|
|
21
|
+
var orchestrator_service_1 = require("./orchestrator.service");
|
|
22
|
+
Object.defineProperty(exports, "OrchestratorService", { enumerable: true, get: function () { return orchestrator_service_1.OrchestratorService; } });
|
|
23
|
+
Object.defineProperty(exports, "OrchestratorError", { enumerable: true, get: function () { return orchestrator_service_1.OrchestratorError; } });
|
|
24
|
+
var agent_job_worker_1 = require("./agent-job.worker");
|
|
25
|
+
Object.defineProperty(exports, "AgentJobWorker", { enumerable: true, get: function () { return agent_job_worker_1.AgentJobWorker; } });
|
|
26
|
+
var chat_token_service_1 = require("./chat-token.service");
|
|
27
|
+
Object.defineProperty(exports, "ChatTokenService", { enumerable: true, get: function () { return chat_token_service_1.ChatTokenService; } });
|
|
28
|
+
Object.defineProperty(exports, "ChatTokenError", { enumerable: true, get: function () { return chat_token_service_1.ChatTokenError; } });
|
|
29
|
+
var mcp_client_service_1 = require("./mcp-client.service");
|
|
30
|
+
Object.defineProperty(exports, "McpClientService", { enumerable: true, get: function () { return mcp_client_service_1.McpClientService; } });
|
|
31
|
+
Object.defineProperty(exports, "McpClientError", { enumerable: true, get: function () { return mcp_client_service_1.McpClientError; } });
|
|
32
|
+
var mcp_server_service_1 = require("./mcp-server.service");
|
|
33
|
+
Object.defineProperty(exports, "McpServerService", { enumerable: true, get: function () { return mcp_server_service_1.McpServerService; } });
|
|
34
|
+
Object.defineProperty(exports, "McpServerError", { enumerable: true, get: function () { return mcp_server_service_1.McpServerError; } });
|
|
35
|
+
var oauth2_service_1 = require("./oauth2.service");
|
|
36
|
+
Object.defineProperty(exports, "OAuth2Service", { enumerable: true, get: function () { return oauth2_service_1.OAuth2Service; } });
|
|
37
|
+
var connector_registry_service_1 = require("./connector-registry.service");
|
|
38
|
+
Object.defineProperty(exports, "ConnectorRegistryService", { enumerable: true, get: function () { return connector_registry_service_1.ConnectorRegistryService; } });
|
|
39
|
+
Object.defineProperty(exports, "ConnectorError", { enumerable: true, get: function () { return connector_registry_service_1.ConnectorError; } });
|
|
40
|
+
Object.defineProperty(exports, "InMemoryAuthorizeStateStore", { enumerable: true, get: function () { return connector_registry_service_1.InMemoryAuthorizeStateStore; } });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Logger } from './tool-registry.service';
|
|
2
|
+
import type { ToolRegistryService } from './tool-registry.service';
|
|
3
|
+
import type { McpServerConfig } from '../types/config.types';
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle owner for connections to remote MCP servers.
|
|
6
|
+
*
|
|
7
|
+
* Boot path:
|
|
8
|
+
* for each McpServerConfig
|
|
9
|
+
* 1. connect (StreamableHTTP first, SSE as opt-in)
|
|
10
|
+
* 2. listTools() — pull the catalog the server publishes
|
|
11
|
+
* 3. wrap each tool into an AgentToolDefinition prefixed with `<name>__`
|
|
12
|
+
* 4. register them all in the ToolRegistryService alongside built-ins
|
|
13
|
+
*
|
|
14
|
+
* The wrapped tools never expose the MCP transport details to the LLM: they
|
|
15
|
+
* look identical to a hand-written tool from the model's perspective. When
|
|
16
|
+
* the LLM invokes one, we route the call through `client.callTool` and hand
|
|
17
|
+
* back whatever the server returned (text, JSON, etc., flattened to string).
|
|
18
|
+
*
|
|
19
|
+
* Connection model is "connect at register time, keep the socket warm".
|
|
20
|
+
* A future revision can swap to lazy-connect-on-first-use if the cost of
|
|
21
|
+
* idle connections matters; for v1, eager connect makes debugging trivial
|
|
22
|
+
* (you see the failure at boot, not at the first agent message).
|
|
23
|
+
*/
|
|
24
|
+
export interface McpToolHandle {
|
|
25
|
+
/** Server name (the `name` field of McpServerConfig). */
|
|
26
|
+
serverName: string;
|
|
27
|
+
/** Original tool name as the MCP server announced it. */
|
|
28
|
+
toolName: string;
|
|
29
|
+
/** Prefixed name as registered in the ToolRegistry. */
|
|
30
|
+
registeredName: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class McpClientError extends Error {
|
|
33
|
+
status: number;
|
|
34
|
+
code: 'connect_failed' | 'list_failed' | 'call_failed';
|
|
35
|
+
constructor(code: McpClientError['code'], message: string);
|
|
36
|
+
}
|
|
37
|
+
export declare class McpClientService {
|
|
38
|
+
private readonly registry;
|
|
39
|
+
private readonly clients;
|
|
40
|
+
private readonly registered;
|
|
41
|
+
private readonly logger;
|
|
42
|
+
constructor(registry: ToolRegistryService, opts?: {
|
|
43
|
+
logger?: Logger;
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Connect, list tools, and register them. Idempotent for a given `name` —
|
|
47
|
+
* a second `register` for the same server tears down the previous client
|
|
48
|
+
* and replaces it. Useful for "edit server settings" flows.
|
|
49
|
+
*/
|
|
50
|
+
register(config: McpServerConfig): Promise<McpToolHandle[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Tear down a server's connection and unregister its tools. Returns the
|
|
53
|
+
* number of tools removed. Safe to call for unknown names — no-ops.
|
|
54
|
+
*/
|
|
55
|
+
unregister(name: string): Promise<number>;
|
|
56
|
+
/** Best-effort shutdown of every connected MCP. Called on app shutdown. */
|
|
57
|
+
shutdown(): Promise<void>;
|
|
58
|
+
list(): {
|
|
59
|
+
name: string;
|
|
60
|
+
tools: McpToolHandle[];
|
|
61
|
+
}[];
|
|
62
|
+
isConnected(name: string): boolean;
|
|
63
|
+
private callTool;
|
|
64
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpClientService = exports.McpClientError = void 0;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
5
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
6
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
7
|
+
class McpClientError extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = 502;
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.McpClientError = McpClientError;
|
|
15
|
+
class McpClientService {
|
|
16
|
+
constructor(registry, opts = {}) {
|
|
17
|
+
this.registry = registry;
|
|
18
|
+
this.clients = new Map();
|
|
19
|
+
this.registered = new Map();
|
|
20
|
+
this.logger = opts.logger ?? {
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
log: (m) => console.log(`[mcp] ${m}`),
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
warn: (m) => console.warn(`[mcp] ${m}`),
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
debug: (m) => console.debug(`[mcp] ${m}`),
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
error: (m) => console.error(`[mcp] ${m}`),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Connect, list tools, and register them. Idempotent for a given `name` —
|
|
34
|
+
* a second `register` for the same server tears down the previous client
|
|
35
|
+
* and replaces it. Useful for "edit server settings" flows.
|
|
36
|
+
*/
|
|
37
|
+
async register(config) {
|
|
38
|
+
await this.unregister(config.name);
|
|
39
|
+
const client = new index_js_1.Client({ name: 'agentforge', version: '0.2.0' }, { capabilities: {} });
|
|
40
|
+
const transport = buildTransport(config);
|
|
41
|
+
try {
|
|
42
|
+
await client.connect(transport);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new McpClientError('connect_failed', `Failed to connect to MCP server "${config.name}": ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
this.clients.set(config.name, client);
|
|
48
|
+
let toolsResult;
|
|
49
|
+
try {
|
|
50
|
+
toolsResult = await client.listTools();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new McpClientError('list_failed', `Failed to list tools from "${config.name}": ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
const handles = [];
|
|
56
|
+
for (const tool of toolsResult.tools) {
|
|
57
|
+
const registeredName = `${config.name}__${tool.name}`;
|
|
58
|
+
const wrapped = {
|
|
59
|
+
name: registeredName,
|
|
60
|
+
description: (tool.description ?? '') +
|
|
61
|
+
` (via MCP server "${config.name}")`,
|
|
62
|
+
// MCP servers publish JSON Schema directly under inputSchema.
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
inputSchema: tool.inputSchema ?? {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {},
|
|
67
|
+
},
|
|
68
|
+
execute: async (input) => {
|
|
69
|
+
return this.callTool(config.name, tool.name, input);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
this.registry.register(wrapped);
|
|
73
|
+
handles.push({
|
|
74
|
+
serverName: config.name,
|
|
75
|
+
toolName: tool.name,
|
|
76
|
+
registeredName,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
this.registered.set(config.name, handles);
|
|
80
|
+
this.logger.log(`[mcp] connected "${config.name}" — registered ${handles.length} tool(s)`);
|
|
81
|
+
return handles;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Tear down a server's connection and unregister its tools. Returns the
|
|
85
|
+
* number of tools removed. Safe to call for unknown names — no-ops.
|
|
86
|
+
*/
|
|
87
|
+
async unregister(name) {
|
|
88
|
+
const handles = this.registered.get(name);
|
|
89
|
+
if (handles) {
|
|
90
|
+
for (const h of handles) {
|
|
91
|
+
this.registry.unregister?.(h.registeredName);
|
|
92
|
+
}
|
|
93
|
+
this.registered.delete(name);
|
|
94
|
+
}
|
|
95
|
+
const client = this.clients.get(name);
|
|
96
|
+
if (client) {
|
|
97
|
+
try {
|
|
98
|
+
await client.close();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.logger.warn(`[mcp] error closing client "${name}": ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
this.clients.delete(name);
|
|
104
|
+
}
|
|
105
|
+
return handles?.length ?? 0;
|
|
106
|
+
}
|
|
107
|
+
/** Best-effort shutdown of every connected MCP. Called on app shutdown. */
|
|
108
|
+
async shutdown() {
|
|
109
|
+
const names = Array.from(this.clients.keys());
|
|
110
|
+
await Promise.all(names.map((n) => this.unregister(n)));
|
|
111
|
+
}
|
|
112
|
+
// ─── Introspection (for the admin UI) ──────────────────────────────────
|
|
113
|
+
list() {
|
|
114
|
+
return Array.from(this.registered.entries()).map(([name, tools]) => ({
|
|
115
|
+
name,
|
|
116
|
+
tools,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
isConnected(name) {
|
|
120
|
+
return this.clients.has(name);
|
|
121
|
+
}
|
|
122
|
+
// ─── Execution ─────────────────────────────────────────────────────────
|
|
123
|
+
async callTool(serverName, toolName, args) {
|
|
124
|
+
const client = this.clients.get(serverName);
|
|
125
|
+
if (!client) {
|
|
126
|
+
throw new McpClientError('call_failed', `MCP server "${serverName}" is not connected.`);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
130
|
+
// MCP returns `content` as an array of typed blocks; we flatten to a
|
|
131
|
+
// string the agent can read. Anything non-text becomes "[type omitted]"
|
|
132
|
+
// — the agent can ask the server for follow-up if it cares.
|
|
133
|
+
const content = (result.content ?? []);
|
|
134
|
+
return content
|
|
135
|
+
.map((c) => (c.type === 'text' ? c.text ?? '' : `[${c.type} omitted]`))
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
throw new McpClientError('call_failed', `MCP "${serverName}/${toolName}" failed: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.McpClientService = McpClientService;
|
|
144
|
+
// ─── Transport factory ────────────────────────────────────────────────────
|
|
145
|
+
function buildTransport(config) {
|
|
146
|
+
const url = new URL(config.url);
|
|
147
|
+
const requestInit = config.headers
|
|
148
|
+
? { headers: config.headers }
|
|
149
|
+
: undefined;
|
|
150
|
+
if (config.transport === 'http') {
|
|
151
|
+
return new streamableHttp_js_1.StreamableHTTPClientTransport(url, { requestInit });
|
|
152
|
+
}
|
|
153
|
+
if (config.transport === 'sse') {
|
|
154
|
+
return new sse_js_1.SSEClientTransport(url, { requestInit });
|
|
155
|
+
}
|
|
156
|
+
throw new McpClientError('connect_failed', `Unsupported MCP transport: ${config.transport}`);
|
|
157
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { McpServerRecord, McpServerRecordPatch } from '../domain/mcp-server';
|
|
2
|
+
import type { McpServerRepository } from '../repositories';
|
|
3
|
+
import type { McpClientService } from './mcp-client.service';
|
|
4
|
+
export declare class McpServerError extends Error {
|
|
5
|
+
status: number;
|
|
6
|
+
code: 'invalid' | 'not_found' | 'duplicate_name';
|
|
7
|
+
constructor(code: McpServerError['code'], message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface CreateMcpServerInput {
|
|
10
|
+
tenantId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
transport: 'http' | 'sse';
|
|
13
|
+
url: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
isActive?: boolean;
|
|
17
|
+
createdByUserId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Application-level CRUD over `McpServerRecord`. The HTTP controllers
|
|
21
|
+
* (admin endpoints) call this — they don't touch the repository directly so
|
|
22
|
+
* validation (name format, uniqueness per tenant) lives in one place.
|
|
23
|
+
*
|
|
24
|
+
* Side effects on the live MCP catalog (connect / disconnect / re-register)
|
|
25
|
+
* are routed through the optional `client` dependency. When omitted (e.g.
|
|
26
|
+
* unit tests), the service is pure persistence.
|
|
27
|
+
*/
|
|
28
|
+
export declare class McpServerService {
|
|
29
|
+
private readonly repo;
|
|
30
|
+
private readonly client?;
|
|
31
|
+
constructor(repo: McpServerRepository, client?: McpClientService | undefined);
|
|
32
|
+
create(input: CreateMcpServerInput): Promise<McpServerRecord>;
|
|
33
|
+
listForTenant(tenantId: string): Promise<McpServerRecord[]>;
|
|
34
|
+
getById(id: string): Promise<McpServerRecord>;
|
|
35
|
+
getByIdForTenant(id: string, tenantId: string): Promise<McpServerRecord>;
|
|
36
|
+
update(id: string, tenantId: string, patch: McpServerRecordPatch): Promise<McpServerRecord>;
|
|
37
|
+
delete(id: string, tenantId: string): Promise<void>;
|
|
38
|
+
/** Boot path: connect to every active server across tenants. Failures
|
|
39
|
+
* are logged but don't abort boot — a broken Notion key shouldn't keep
|
|
40
|
+
* the rest of the platform from coming up. */
|
|
41
|
+
registerAllActive(): Promise<void>;
|
|
42
|
+
private assertName;
|
|
43
|
+
private assertUrl;
|
|
44
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpServerService = exports.McpServerError = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
class McpServerError extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status =
|
|
10
|
+
code === 'not_found' ? 404 : code === 'duplicate_name' ? 409 : 400;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.McpServerError = McpServerError;
|
|
14
|
+
// Match the McpClientService prefix rule: lowercase alphanumeric + underscore,
|
|
15
|
+
// 2..32 chars. The name becomes a tool-name prefix so we want it cheap to read.
|
|
16
|
+
const NAME_RE = /^[a-z][a-z0-9_]{1,31}$/;
|
|
17
|
+
/**
|
|
18
|
+
* Application-level CRUD over `McpServerRecord`. The HTTP controllers
|
|
19
|
+
* (admin endpoints) call this — they don't touch the repository directly so
|
|
20
|
+
* validation (name format, uniqueness per tenant) lives in one place.
|
|
21
|
+
*
|
|
22
|
+
* Side effects on the live MCP catalog (connect / disconnect / re-register)
|
|
23
|
+
* are routed through the optional `client` dependency. When omitted (e.g.
|
|
24
|
+
* unit tests), the service is pure persistence.
|
|
25
|
+
*/
|
|
26
|
+
class McpServerService {
|
|
27
|
+
constructor(repo, client) {
|
|
28
|
+
this.repo = repo;
|
|
29
|
+
this.client = client;
|
|
30
|
+
}
|
|
31
|
+
async create(input) {
|
|
32
|
+
this.assertName(input.name);
|
|
33
|
+
this.assertUrl(input.url);
|
|
34
|
+
const existing = (await this.repo.listForTenant(input.tenantId)).find((r) => r.name === input.name);
|
|
35
|
+
if (existing) {
|
|
36
|
+
throw new McpServerError('duplicate_name', `An MCP server named "${input.name}" already exists for this tenant.`);
|
|
37
|
+
}
|
|
38
|
+
const record = {
|
|
39
|
+
id: (0, crypto_1.randomUUID)(),
|
|
40
|
+
tenantId: input.tenantId,
|
|
41
|
+
name: input.name,
|
|
42
|
+
transport: input.transport,
|
|
43
|
+
url: input.url,
|
|
44
|
+
description: input.description?.trim() || undefined,
|
|
45
|
+
headers: input.headers,
|
|
46
|
+
isActive: input.isActive ?? true,
|
|
47
|
+
createdByUserId: input.createdByUserId,
|
|
48
|
+
};
|
|
49
|
+
const saved = await this.repo.create(record);
|
|
50
|
+
if (saved.isActive && this.client) {
|
|
51
|
+
// Connect eagerly — if it fails we still keep the record so the admin
|
|
52
|
+
// can fix headers/URL and retry, but we surface the error.
|
|
53
|
+
await this.client.register(toClientConfig(saved));
|
|
54
|
+
}
|
|
55
|
+
return saved;
|
|
56
|
+
}
|
|
57
|
+
async listForTenant(tenantId) {
|
|
58
|
+
return this.repo.listForTenant(tenantId);
|
|
59
|
+
}
|
|
60
|
+
async getById(id) {
|
|
61
|
+
const r = await this.repo.findById(id);
|
|
62
|
+
if (!r)
|
|
63
|
+
throw new McpServerError('not_found', 'MCP server not found');
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
async getByIdForTenant(id, tenantId) {
|
|
67
|
+
const r = await this.getById(id);
|
|
68
|
+
if (r.tenantId !== tenantId) {
|
|
69
|
+
throw new McpServerError('not_found', 'MCP server not found');
|
|
70
|
+
}
|
|
71
|
+
return r;
|
|
72
|
+
}
|
|
73
|
+
async update(id, tenantId, patch) {
|
|
74
|
+
const current = await this.getByIdForTenant(id, tenantId);
|
|
75
|
+
if (patch.name !== undefined && patch.name !== current.name) {
|
|
76
|
+
this.assertName(patch.name);
|
|
77
|
+
const dup = (await this.repo.listForTenant(tenantId)).find((r) => r.name === patch.name && r.id !== id);
|
|
78
|
+
if (dup) {
|
|
79
|
+
throw new McpServerError('duplicate_name', `An MCP server named "${patch.name}" already exists for this tenant.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (patch.url !== undefined)
|
|
83
|
+
this.assertUrl(patch.url);
|
|
84
|
+
await this.repo.update(id, patch);
|
|
85
|
+
const updated = await this.getById(id);
|
|
86
|
+
// Re-register on the live client. The McpClientService's `register` is
|
|
87
|
+
// idempotent — it tears down the previous client first.
|
|
88
|
+
if (this.client) {
|
|
89
|
+
if (updated.isActive) {
|
|
90
|
+
await this.client.register(toClientConfig(updated));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await this.client.unregister(updated.name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return updated;
|
|
97
|
+
}
|
|
98
|
+
async delete(id, tenantId) {
|
|
99
|
+
const record = await this.getByIdForTenant(id, tenantId);
|
|
100
|
+
if (this.client) {
|
|
101
|
+
await this.client.unregister(record.name);
|
|
102
|
+
}
|
|
103
|
+
await this.repo.delete(id);
|
|
104
|
+
}
|
|
105
|
+
/** Boot path: connect to every active server across tenants. Failures
|
|
106
|
+
* are logged but don't abort boot — a broken Notion key shouldn't keep
|
|
107
|
+
* the rest of the platform from coming up. */
|
|
108
|
+
async registerAllActive() {
|
|
109
|
+
if (!this.client)
|
|
110
|
+
return;
|
|
111
|
+
const active = await this.repo.listActive();
|
|
112
|
+
for (const record of active) {
|
|
113
|
+
try {
|
|
114
|
+
await this.client.register(toClientConfig(record));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// McpClientService already logs the connect_failed reason; keep going.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ─── Internals ────────────────────────────────────────────────────────
|
|
122
|
+
assertName(name) {
|
|
123
|
+
if (!NAME_RE.test(name)) {
|
|
124
|
+
throw new McpServerError('invalid', 'name must be 2–32 chars, lowercase letters/digits/underscore, start with a letter');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
assertUrl(url) {
|
|
128
|
+
try {
|
|
129
|
+
const u = new URL(url);
|
|
130
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
131
|
+
throw new Error('protocol must be http or https');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
throw new McpServerError('invalid', `Invalid URL: ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.McpServerService = McpServerService;
|
|
140
|
+
function toClientConfig(r) {
|
|
141
|
+
return {
|
|
142
|
+
name: r.name,
|
|
143
|
+
transport: r.transport,
|
|
144
|
+
url: r.url,
|
|
145
|
+
headers: r.headers,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { OAuthProviderConfig } from '../types/config.types';
|
|
2
|
+
/**
|
|
3
|
+
* Framework-free OAuth 2.0 helper. Provider specs + token exchange +
|
|
4
|
+
* profile normalization live here so every transport (Express, Nest,
|
|
5
|
+
* Fastify, …) drives the same code path. Adapters bring the HTTP shell:
|
|
6
|
+
* - render an authorize redirect with the state we hand back
|
|
7
|
+
* - persist that state (cookie, KV, whatever) until the callback
|
|
8
|
+
* - call validateCallback on the way back in
|
|
9
|
+
*
|
|
10
|
+
* CSRF: the state must round-trip through the user-agent in a way the
|
|
11
|
+
* server can verify. The Express adapter uses an httpOnly cookie scoped
|
|
12
|
+
* to the callback path. The Nest adapter does the same. Custom adapters
|
|
13
|
+
* are free to pick a different mechanism as long as `state` is opaque,
|
|
14
|
+
* single-use, and tied to the originating browser.
|
|
15
|
+
*/
|
|
16
|
+
export interface NormalizedOAuthProfile {
|
|
17
|
+
provider: string;
|
|
18
|
+
providerId: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
emailVerified?: boolean;
|
|
21
|
+
name?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface OAuthProviderSpec {
|
|
25
|
+
/** Identifier used in the URL: `/auth/{name}`. */
|
|
26
|
+
name: string;
|
|
27
|
+
authorizeUrl: string;
|
|
28
|
+
tokenUrl: string;
|
|
29
|
+
/** Optional second fetch — useful for GitHub where /user may not include email. */
|
|
30
|
+
fetchProfile: (accessToken: string) => Promise<NormalizedOAuthProfile>;
|
|
31
|
+
/** Extra params for the authorize redirect (scopes, prompts, etc.). */
|
|
32
|
+
authorizeExtras: () => Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Public name of the state cookie/header per provider. Adapters share the
|
|
36
|
+
* convention so a session started under one transport could (in theory) be
|
|
37
|
+
* resumed under another — and so the operations docs are consistent.
|
|
38
|
+
*/
|
|
39
|
+
export declare const OAUTH_STATE_COOKIE_PREFIX = "af_oauth_state_";
|
|
40
|
+
export declare const OAUTH_STATE_TTL_MS: number;
|
|
41
|
+
/** Build the authorize URL + a fresh random state for the cookie/store. */
|
|
42
|
+
export declare function buildAuthorizeUrl(spec: OAuthProviderSpec, cfg: OAuthProviderConfig): {
|
|
43
|
+
authorizeUrl: string;
|
|
44
|
+
state: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Validate the callback against the expected state and exchange the
|
|
48
|
+
* authorization code for an access token + normalized profile.
|
|
49
|
+
*
|
|
50
|
+
* Returns the profile on success. Throws OAuthCallbackError with a stable
|
|
51
|
+
* `.code` on failure — adapters surface the code in the failure redirect.
|
|
52
|
+
*/
|
|
53
|
+
export declare function completeAuthorization(input: {
|
|
54
|
+
spec: OAuthProviderSpec;
|
|
55
|
+
cfg: OAuthProviderConfig;
|
|
56
|
+
/** Querystring `state` sent back by the provider. */
|
|
57
|
+
returnedState: string | undefined;
|
|
58
|
+
/** State we persisted before redirecting to the provider. */
|
|
59
|
+
expectedState: string | undefined;
|
|
60
|
+
/** Querystring `code` from the provider. */
|
|
61
|
+
code: string | undefined;
|
|
62
|
+
/** Querystring `error` from the provider, if any. */
|
|
63
|
+
providerError: string | undefined;
|
|
64
|
+
}): Promise<NormalizedOAuthProfile>;
|
|
65
|
+
/** Stable error shape adapters can pattern-match on for the failure redirect. */
|
|
66
|
+
export declare class OAuthCallbackError extends Error {
|
|
67
|
+
code: string;
|
|
68
|
+
constructor(code: string, message: string);
|
|
69
|
+
}
|
|
70
|
+
export declare const GOOGLE_OAUTH_SPEC: OAuthProviderSpec;
|
|
71
|
+
export declare const GITHUB_OAUTH_SPEC: OAuthProviderSpec;
|
|
72
|
+
/** Look up a built-in spec by name. Returns undefined for unknown names. */
|
|
73
|
+
export declare function getOAuthProviderSpec(name: string): OAuthProviderSpec | undefined;
|