omnikey-cli 1.0.39 → 1.0.41

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.
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ // MCP client runtime.
3
+ //
4
+ // Maintains long-lived connections to each user-configured MCP server and
5
+ // exposes their tools to the agent as `AITool` entries. The agent's tool
6
+ // dispatcher routes any tool call whose name starts with `MCP_TOOL_PREFIX`
7
+ // back here so it is forwarded to the originating MCP server.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MCP_TOOL_PREFIX = void 0;
10
+ exports.getMcpToolsForSubscription = getMcpToolsForSubscription;
11
+ exports.executeMcpTool = executeMcpTool;
12
+ exports.invalidateMcpRuntimeForServer = invalidateMcpRuntimeForServer;
13
+ exports.shutdownAllMcpClients = shutdownAllMcpClients;
14
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
15
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
16
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
17
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
18
+ const config_1 = require("../config");
19
+ const mcpServer_1 = require("../models/mcpServer");
20
+ exports.MCP_TOOL_PREFIX = 'mcp_';
21
+ const MAX_TOOL_NAME_LEN = 64;
22
+ const CONNECT_TIMEOUT_MS = 15000;
23
+ const clients = new Map(); // by MCPServer.id
24
+ function slug(s) {
25
+ return s
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '_')
28
+ .replace(/^_+|_+$/g, '')
29
+ .slice(0, 30);
30
+ }
31
+ function buildToolName(serverName, toolName) {
32
+ const candidate = `${exports.MCP_TOOL_PREFIX}${slug(serverName)}__${slug(toolName)}`;
33
+ return candidate.slice(0, MAX_TOOL_NAME_LEN);
34
+ }
35
+ function isStdioAllowed() {
36
+ // Spawning arbitrary child processes is only safe in single-tenant (self-hosted)
37
+ // deployments. On a shared SaaS backend, stdio servers are disabled — only
38
+ // outbound HTTP/SSE transports are permitted.
39
+ return config_1.config.isSelfHosted === true || config_1.config.isLocal === true;
40
+ }
41
+ async function connectOne(server, log) {
42
+ try {
43
+ if (server.transport === 'stdio' && !isStdioAllowed()) {
44
+ throw new Error('stdio MCP transport is disabled in this deployment.');
45
+ }
46
+ const client = new index_js_1.Client({ name: 'omnikey-agent', version: '1.0.0' }, { capabilities: {} });
47
+ if (server.transport === 'stdio') {
48
+ if (!server.command)
49
+ throw new Error('command is required for stdio transport');
50
+ const transport = new stdio_js_1.StdioClientTransport({
51
+ command: server.command,
52
+ args: server.args ?? [],
53
+ // Pass-through the user-provided env in addition to a safe default set.
54
+ env: { ...process.env, ...(server.env ?? {}) },
55
+ stderr: 'pipe',
56
+ });
57
+ await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, 'MCP stdio connect');
58
+ }
59
+ else if (server.transport === 'http') {
60
+ if (!server.url)
61
+ throw new Error('url is required for http transport');
62
+ const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(server.url), {
63
+ requestInit: { headers: server.headers ?? {} },
64
+ });
65
+ await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, 'MCP http connect');
66
+ }
67
+ else {
68
+ if (!server.url)
69
+ throw new Error('url is required for sse transport');
70
+ const transport = new sse_js_1.SSEClientTransport(new URL(server.url), {
71
+ requestInit: { headers: server.headers ?? {} },
72
+ });
73
+ await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, 'MCP sse connect');
74
+ }
75
+ const listed = await withTimeout(client.listTools(), CONNECT_TIMEOUT_MS, 'MCP listTools');
76
+ const tools = (listed.tools ?? []).map((t) => ({
77
+ name: t.name,
78
+ description: t.description,
79
+ inputSchema: (t.inputSchema ?? { type: 'object', properties: {} }),
80
+ }));
81
+ await mcpServer_1.MCPServer.update({ lastConnectedAt: new Date(), lastError: null }, { where: { id: server.id } }).catch(() => undefined);
82
+ log.info('Connected to MCP server', {
83
+ mcpServerId: server.id,
84
+ mcpServerName: server.name,
85
+ transport: server.transport,
86
+ toolCount: tools.length,
87
+ });
88
+ return { serverId: server.id, serverName: server.name, client, tools };
89
+ }
90
+ catch (err) {
91
+ const message = err instanceof Error ? err.message : String(err);
92
+ log.warn('Failed to connect to MCP server', {
93
+ mcpServerId: server.id,
94
+ mcpServerName: server.name,
95
+ transport: server.transport,
96
+ error: message,
97
+ });
98
+ await mcpServer_1.MCPServer.update({ lastError: message }, { where: { id: server.id } }).catch(() => undefined);
99
+ return null;
100
+ }
101
+ }
102
+ async function getOrConnect(server, log) {
103
+ const cached = clients.get(server.id);
104
+ if (cached)
105
+ return cached;
106
+ const connected = await connectOne(server, log);
107
+ if (connected)
108
+ clients.set(server.id, connected);
109
+ return connected;
110
+ }
111
+ /**
112
+ * Builds the set of AI tool definitions exposed to the LLM for one subscription.
113
+ * Returns both the tool definitions and a dispatch map used by `executeMcpTool`
114
+ * to route a tool call back to the right (server, mcpToolName) pair.
115
+ */
116
+ async function getMcpToolsForSubscription(subscriptionId, log) {
117
+ const aiTools = [];
118
+ const dispatch = new Map();
119
+ let servers;
120
+ try {
121
+ servers = await mcpServer_1.MCPServer.findAll({
122
+ where: { subscriptionId, isEnabled: true },
123
+ });
124
+ }
125
+ catch (err) {
126
+ log.error('Failed to load MCP servers for runtime', { error: err });
127
+ return { aiTools, dispatch };
128
+ }
129
+ // Connect / re-use clients in parallel.
130
+ const connected = await Promise.all(servers.map((s) => getOrConnect(s, log)));
131
+ for (const c of connected) {
132
+ if (!c)
133
+ continue;
134
+ for (const tool of c.tools) {
135
+ const toolName = buildToolName(c.serverName, tool.name);
136
+ if (dispatch.has(toolName)) {
137
+ log.warn('MCP tool name collision — skipping', {
138
+ toolName,
139
+ mcpServerName: c.serverName,
140
+ mcpToolName: tool.name,
141
+ });
142
+ continue;
143
+ }
144
+ dispatch.set(toolName, { serverId: c.serverId, mcpToolName: tool.name });
145
+ aiTools.push({
146
+ name: toolName,
147
+ description: tool.description
148
+ ? `[${c.serverName}] ${tool.description}`
149
+ : `[${c.serverName}] MCP tool ${tool.name}`,
150
+ parameters: tool.inputSchema,
151
+ });
152
+ }
153
+ }
154
+ return { aiTools, dispatch };
155
+ }
156
+ /**
157
+ * Executes a previously-advertised MCP tool. `dispatch` must be the same map
158
+ * produced by `getMcpToolsForSubscription` for this turn (so we know which
159
+ * server and underlying tool name to forward to).
160
+ */
161
+ async function executeMcpTool(toolName, args, dispatch, log) {
162
+ const entry = dispatch.get(toolName);
163
+ if (!entry) {
164
+ return `Error: unknown MCP tool "${toolName}".`;
165
+ }
166
+ const client = clients.get(entry.serverId);
167
+ if (!client) {
168
+ return `Error: MCP server for tool "${toolName}" is not connected.`;
169
+ }
170
+ try {
171
+ const result = await withTimeout(client.client.callTool({ name: entry.mcpToolName, arguments: args }), 60000, `MCP callTool ${toolName}`);
172
+ return stringifyMcpToolResult(result);
173
+ }
174
+ catch (err) {
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ log.warn('MCP tool call failed', {
177
+ toolName,
178
+ mcpServerId: entry.serverId,
179
+ mcpToolName: entry.mcpToolName,
180
+ error: message,
181
+ });
182
+ return `Error invoking MCP tool ${toolName}: ${message}`;
183
+ }
184
+ }
185
+ /**
186
+ * Disconnect (and forget) any cached client for the given MCP server id. Called
187
+ * by the CRUD routes after an MCP server row is updated or deleted so the next
188
+ * agent turn picks up the new config.
189
+ */
190
+ async function invalidateMcpRuntimeForServer(serverId) {
191
+ const existing = clients.get(serverId);
192
+ if (!existing)
193
+ return;
194
+ clients.delete(serverId);
195
+ try {
196
+ await existing.client.close();
197
+ }
198
+ catch {
199
+ // ignore — the client may already be torn down.
200
+ }
201
+ }
202
+ /**
203
+ * Disconnect every cached client. Intended for graceful shutdown and tests.
204
+ */
205
+ async function shutdownAllMcpClients() {
206
+ const all = Array.from(clients.values());
207
+ clients.clear();
208
+ await Promise.all(all.map((c) => c.client.close().catch(() => {
209
+ // ignore
210
+ })));
211
+ }
212
+ function stringifyMcpToolResult(result) {
213
+ if (!result || typeof result !== 'object')
214
+ return String(result ?? '');
215
+ const r = result;
216
+ if (Array.isArray(r.content)) {
217
+ const parts = r.content.map((item) => {
218
+ if (item && typeof item === 'object' && 'type' in item) {
219
+ const i = item;
220
+ if (i.type === 'text' && typeof i.text === 'string')
221
+ return i.text;
222
+ if (i.type === 'image')
223
+ return `[image: ${i.mimeType ?? 'unknown'}]`;
224
+ if (i.type === 'resource')
225
+ return `[resource]`;
226
+ }
227
+ return JSON.stringify(item);
228
+ });
229
+ const joined = parts.join('\n');
230
+ return r.isError ? `Error from MCP tool: ${joined}` : joined;
231
+ }
232
+ return JSON.stringify(result);
233
+ }
234
+ function withTimeout(p, ms, label) {
235
+ return new Promise((resolve, reject) => {
236
+ const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
237
+ p.then((v) => {
238
+ clearTimeout(t);
239
+ resolve(v);
240
+ }, (e) => {
241
+ clearTimeout(t);
242
+ reject(e);
243
+ });
244
+ });
245
+ }
@@ -18,8 +18,8 @@ const imageTool_1 = require("./imageTool");
18
18
  *
19
19
  * @returns An array of `AITool` definitions ready to pass to the AI client.
20
20
  */
21
- function buildAvailableTools() {
22
- return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL, imageTool_1.IMAGE_GENERATE_TOOL];
21
+ function buildAvailableTools(extraTools = []) {
22
+ return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL, imageTool_1.IMAGE_GENERATE_TOOL, ...extraTools];
23
23
  }
24
24
  /**
25
25
  * Strips the `@omniagent` mention from user-supplied content.
@@ -14,12 +14,14 @@ const db_1 = require("./db");
14
14
  const logger_1 = require("./logger");
15
15
  const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
16
16
  const scheduledJobRoutes_1 = require("./scheduledJobRoutes");
17
+ const mcpServerRoutes_1 = require("./mcpServerRoutes");
17
18
  const scheduledJobExecutor_1 = require("./scheduledJobExecutor");
18
19
  const config_1 = require("./config");
19
20
  const agentServer_1 = require("./agent/agentServer");
20
21
  // Importing AgentSession and ScheduledJob ensures the models are registered with Sequelize before initDatabase().
21
22
  require("./models/agentSession");
22
23
  require("./models/scheduledJob");
24
+ require("./models/mcpServer");
23
25
  const bucket_adapter_1 = require("./bucket-adapter");
24
26
  const app = (0, express_1.default)();
25
27
  const PORT = Number(config_1.config.port);
@@ -32,6 +34,7 @@ app.use('/api/subscription', (0, subscriptionRoutes_1.createSubscriptionRouter)(
32
34
  app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
33
35
  app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
34
36
  app.use('/api/scheduled-jobs', (0, scheduledJobRoutes_1.scheduledJobRouter)());
37
+ app.use('/api/mcp-servers', (0, mcpServerRoutes_1.mcpServerRouter)());
35
38
  app.use('/api/agent', (0, agentServer_1.createAgentRouter)());
36
39
  app.get('/macos/download', (_req, res) => {
37
40
  const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
@@ -74,8 +77,8 @@ app.get('/macos/appcast', (req, res) => {
74
77
  const appcastUrl = `${baseUrl}/macos/appcast`;
75
78
  // These should match the values embedded into the macOS app
76
79
  // Info.plist in macOS/build_release_dmg.sh.
77
- const bundleVersion = '26';
78
- const shortVersion = '1.0.25';
80
+ const bundleVersion = '29';
81
+ const shortVersion = '1.0.28';
79
82
  const xml = `<?xml version="1.0" encoding="utf-8"?>
80
83
  <rss version="2.0"
81
84
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
@@ -103,7 +106,7 @@ app.get('/macos/appcast', (req, res) => {
103
106
  // ── Windows distribution endpoints ───────────────────────────────────────────
104
107
  // These should match the values in windows/OmniKey.Windows.csproj
105
108
  // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
106
- const WIN_VERSION = '1.9';
109
+ const WIN_VERSION = '1.11';
107
110
  const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
108
111
  const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
109
112
  // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
@@ -145,7 +148,7 @@ app.get('/windows/update', (req, res) => {
145
148
  version: WIN_VERSION,
146
149
  downloadUrl: `${baseUrl}/windows/download`,
147
150
  fileSize,
148
- releaseNotes: `What's new in ${WIN_VERSION}\n\n• New cron job automation (Scheduled Jobs) — create recurring jobs with cron-style schedules or one-time jobs to run prompts automatically in the background.\n• Scheduled Jobs controls — add jobs, activate/deactivate them, run now on demand, refresh status, and view last-run history in the app.\n OmniAgent session management choose to start a new session or resume an existing one each time you run @omniAgent. Save a default to skip the picker automatically on future runs.\n• History button in the OmniAgent window — change your default session at any time without re-running the agent.\n• OmniAgent Session tray menu item — open session settings directly from the system tray.\n• Left-clicking the tray icon now opens the menu (previously right-click only).\n• Manual updated with detailed OmniAgent, session management, web search provider, and LLM provider documentation.`,
151
+ releaseNotes: `What's new in ${WIN_VERSION}\n\n• OmniAgent flow improvements\n• Bug fixes and performance enhancements\n\n Support for MCP servers now you can add any custom MCP server to OmniKeyAI using CLI or Windows app.`,
149
152
  });
150
153
  });
151
154
  app.get('/downloads/stats', async (_req, res) => {
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.mcpServerRouter = mcpServerRouter;
7
+ const express_1 = __importDefault(require("express"));
8
+ const zod_1 = __importDefault(require("zod"));
9
+ const authMiddleware_1 = require("./authMiddleware");
10
+ const mcpServer_1 = require("./models/mcpServer");
11
+ const mcpPromptCache_1 = require("./agent/mcpPromptCache");
12
+ const mcpRuntime_1 = require("./agent/mcpRuntime");
13
+ const transportEnum = zod_1.default.enum(['stdio', 'http', 'sse']);
14
+ const urlSchema = zod_1.default
15
+ .string()
16
+ .max(1000)
17
+ .url({ message: 'url must be a valid URL.' })
18
+ .refine((v) => /^https?:\/\//i.test(v), {
19
+ message: 'url must use http:// or https:// scheme.',
20
+ });
21
+ const baseSchema = zod_1.default.object({
22
+ name: zod_1.default.string().min(1).max(100),
23
+ description: zod_1.default.string().max(500).nullable().optional(),
24
+ transport: transportEnum.optional(),
25
+ command: zod_1.default.string().max(500).nullable().optional(),
26
+ args: zod_1.default.array(zod_1.default.string()).optional(),
27
+ env: zod_1.default.record(zod_1.default.string(), zod_1.default.string()).optional(),
28
+ url: urlSchema.nullable().optional(),
29
+ headers: zod_1.default.record(zod_1.default.string(), zod_1.default.string()).optional(),
30
+ isEnabled: zod_1.default.boolean().optional(),
31
+ });
32
+ function validateTransportFields(transport, command, url) {
33
+ if (transport === 'stdio') {
34
+ if (!command || !command.trim())
35
+ return 'command is required when transport is "stdio".';
36
+ }
37
+ else {
38
+ if (!url || !url.trim())
39
+ return `url is required when transport is "${transport}".`;
40
+ }
41
+ return null;
42
+ }
43
+ // NOTE: `env` and `headers` may contain secrets (API tokens, credentials, etc.). They are
44
+ // stored at rest in the application database and should be treated as sensitive. List
45
+ // responses redact secret values by default to reduce accidental disclosure; single-record
46
+ // reads return full values so that edit UIs can round-trip the configuration.
47
+ const REDACTED = '***';
48
+ function redactDict(dict) {
49
+ if (!dict)
50
+ return {};
51
+ const out = {};
52
+ for (const key of Object.keys(dict)) {
53
+ out[key] = REDACTED;
54
+ }
55
+ return out;
56
+ }
57
+ function formatServer(server, options = {}) {
58
+ const redact = options.redactSecrets === true;
59
+ return {
60
+ id: server.id,
61
+ name: server.name,
62
+ description: server.description,
63
+ transport: server.transport,
64
+ command: server.command,
65
+ args: server.args,
66
+ env: redact ? redactDict(server.env) : server.env,
67
+ url: server.url,
68
+ headers: redact ? redactDict(server.headers) : server.headers,
69
+ isEnabled: server.isEnabled,
70
+ lastConnectedAt: server.lastConnectedAt,
71
+ lastError: server.lastError,
72
+ createdAt: server.createdAt,
73
+ updatedAt: server.updatedAt,
74
+ };
75
+ }
76
+ function mcpServerRouter() {
77
+ const router = express_1.default.Router();
78
+ router.get('/', authMiddleware_1.authMiddleware, async (_req, res) => {
79
+ const { logger, subscription } = res.locals;
80
+ try {
81
+ const servers = await mcpServer_1.MCPServer.findAll({
82
+ where: { subscriptionId: subscription.id },
83
+ order: [['name', 'ASC']],
84
+ });
85
+ res.json({ servers: servers.map((s) => formatServer(s, { redactSecrets: true })) });
86
+ }
87
+ catch (err) {
88
+ logger.error('Error retrieving MCP servers.', { error: err });
89
+ res.status(500).json({ error: 'Failed to retrieve MCP servers.' });
90
+ }
91
+ });
92
+ router.post('/', authMiddleware_1.authMiddleware, async (req, res) => {
93
+ const { logger, subscription } = res.locals;
94
+ try {
95
+ const parsed = baseSchema.parse(req.body);
96
+ const transport = parsed.transport ?? 'stdio';
97
+ const command = parsed.command ?? null;
98
+ const url = parsed.url ?? null;
99
+ const validationError = validateTransportFields(transport, command, url);
100
+ if (validationError) {
101
+ return res.status(400).json({ error: validationError });
102
+ }
103
+ const server = await mcpServer_1.MCPServer.create({
104
+ subscriptionId: subscription.id,
105
+ name: parsed.name.trim(),
106
+ description: parsed.description ?? null,
107
+ transport,
108
+ command,
109
+ args: parsed.args ?? [],
110
+ env: parsed.env ?? {},
111
+ url,
112
+ headers: parsed.headers ?? {},
113
+ isEnabled: parsed.isEnabled ?? true,
114
+ });
115
+ (0, mcpPromptCache_1.invalidatePromptMcps)(subscription.id);
116
+ void (0, mcpRuntime_1.invalidateMcpRuntimeForServer)(server.id);
117
+ res.status(201).json(formatServer(server));
118
+ }
119
+ catch (err) {
120
+ logger.error('Error creating MCP server.', { error: err });
121
+ if (err instanceof zod_1.default.ZodError) {
122
+ return res.status(400).json({ error: 'Invalid MCP server data.' });
123
+ }
124
+ if (err?.name === 'SequelizeUniqueConstraintError') {
125
+ return res.status(409).json({ error: 'An MCP server with that name already exists.' });
126
+ }
127
+ res.status(500).json({ error: 'Failed to create MCP server.' });
128
+ }
129
+ });
130
+ router.get('/:id', authMiddleware_1.authMiddleware, async (req, res) => {
131
+ const { logger, subscription } = res.locals;
132
+ const { id } = req.params;
133
+ try {
134
+ const server = await mcpServer_1.MCPServer.findOne({
135
+ where: { id, subscriptionId: subscription.id },
136
+ });
137
+ if (!server) {
138
+ return res.status(404).json({ error: 'MCP server not found.' });
139
+ }
140
+ res.json(formatServer(server));
141
+ }
142
+ catch (err) {
143
+ logger.error('Error retrieving MCP server.', { error: err });
144
+ res.status(500).json({ error: 'Failed to retrieve MCP server.' });
145
+ }
146
+ });
147
+ router.patch('/:id', authMiddleware_1.authMiddleware, async (req, res) => {
148
+ const { logger, subscription } = res.locals;
149
+ const { id } = req.params;
150
+ try {
151
+ const parsed = baseSchema.partial().parse(req.body);
152
+ const server = await mcpServer_1.MCPServer.findOne({
153
+ where: { id, subscriptionId: subscription.id },
154
+ });
155
+ if (!server) {
156
+ return res.status(404).json({ error: 'MCP server not found.' });
157
+ }
158
+ const transport = parsed.transport ?? server.transport;
159
+ const transportChanged = parsed.transport !== undefined && parsed.transport !== server.transport;
160
+ // When transport changes, clear fields incompatible with the new transport so
161
+ // stale credentials/config never persist across a transport switch.
162
+ const command = transportChanged
163
+ ? (transport === 'stdio' ? (parsed.command !== undefined ? parsed.command : server.command) : null)
164
+ : (parsed.command !== undefined ? parsed.command : server.command);
165
+ const url = transportChanged
166
+ ? (transport !== 'stdio' ? (parsed.url !== undefined ? parsed.url : server.url) : null)
167
+ : (parsed.url !== undefined ? parsed.url : server.url);
168
+ const args = transportChanged && transport !== 'stdio' ? [] : (parsed.args ?? server.args);
169
+ const env = transportChanged && transport !== 'stdio' ? {} : (parsed.env ?? server.env);
170
+ const headers = transportChanged && transport === 'stdio' ? {} : (parsed.headers ?? server.headers);
171
+ const validationError = validateTransportFields(transport, command, url);
172
+ if (validationError) {
173
+ return res.status(400).json({ error: validationError });
174
+ }
175
+ await server.update({
176
+ name: parsed.name !== undefined ? parsed.name.trim() : server.name,
177
+ description: parsed.description !== undefined ? parsed.description : server.description,
178
+ transport,
179
+ command,
180
+ args,
181
+ env,
182
+ url,
183
+ headers,
184
+ isEnabled: parsed.isEnabled ?? server.isEnabled,
185
+ });
186
+ (0, mcpPromptCache_1.invalidatePromptMcps)(subscription.id);
187
+ void (0, mcpRuntime_1.invalidateMcpRuntimeForServer)(server.id);
188
+ res.json(formatServer(server));
189
+ }
190
+ catch (err) {
191
+ logger.error('Error updating MCP server.', { error: err });
192
+ if (err instanceof zod_1.default.ZodError) {
193
+ return res.status(400).json({ error: 'Invalid MCP server data.' });
194
+ }
195
+ if (err?.name === 'SequelizeUniqueConstraintError') {
196
+ return res.status(409).json({ error: 'An MCP server with that name already exists.' });
197
+ }
198
+ res.status(500).json({ error: 'Failed to update MCP server.' });
199
+ }
200
+ });
201
+ router.delete('/:id', authMiddleware_1.authMiddleware, async (req, res) => {
202
+ const { logger, subscription } = res.locals;
203
+ const { id } = req.params;
204
+ try {
205
+ const server = await mcpServer_1.MCPServer.findOne({
206
+ where: { id, subscriptionId: subscription.id },
207
+ });
208
+ if (!server) {
209
+ return res.status(404).json({ error: 'MCP server not found.' });
210
+ }
211
+ await server.destroy();
212
+ (0, mcpPromptCache_1.invalidatePromptMcps)(subscription.id);
213
+ void (0, mcpRuntime_1.invalidateMcpRuntimeForServer)(server.id);
214
+ res.status(204).send();
215
+ }
216
+ catch (err) {
217
+ logger.error('Error deleting MCP server.', { error: err });
218
+ res.status(500).json({ error: 'Failed to delete MCP server.' });
219
+ }
220
+ });
221
+ return router;
222
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MCPServer = void 0;
7
+ const sequelize_1 = require("sequelize");
8
+ const cuid_1 = __importDefault(require("cuid"));
9
+ const db_1 = require("../db");
10
+ const subscription_1 = require("./subscription");
11
+ class MCPServer extends sequelize_1.Model {
12
+ }
13
+ exports.MCPServer = MCPServer;
14
+ MCPServer.init({
15
+ id: {
16
+ type: sequelize_1.DataTypes.STRING,
17
+ primaryKey: true,
18
+ allowNull: false,
19
+ defaultValue: () => (0, cuid_1.default)(),
20
+ },
21
+ subscriptionId: {
22
+ type: sequelize_1.DataTypes.STRING,
23
+ allowNull: false,
24
+ field: 'subscription_id',
25
+ references: {
26
+ model: subscription_1.Subscription,
27
+ key: 'id',
28
+ },
29
+ onDelete: 'CASCADE',
30
+ onUpdate: 'CASCADE',
31
+ },
32
+ name: {
33
+ type: sequelize_1.DataTypes.STRING(100),
34
+ allowNull: false,
35
+ },
36
+ description: {
37
+ type: sequelize_1.DataTypes.STRING(500),
38
+ allowNull: true,
39
+ },
40
+ transport: {
41
+ type: sequelize_1.DataTypes.STRING(16),
42
+ allowNull: false,
43
+ defaultValue: 'stdio',
44
+ },
45
+ command: {
46
+ type: sequelize_1.DataTypes.STRING(500),
47
+ allowNull: true,
48
+ },
49
+ args: {
50
+ type: sequelize_1.DataTypes.JSON,
51
+ allowNull: false,
52
+ defaultValue: [],
53
+ },
54
+ env: {
55
+ type: sequelize_1.DataTypes.JSON,
56
+ allowNull: false,
57
+ defaultValue: {},
58
+ },
59
+ url: {
60
+ type: sequelize_1.DataTypes.STRING(1000),
61
+ allowNull: true,
62
+ },
63
+ headers: {
64
+ type: sequelize_1.DataTypes.JSON,
65
+ allowNull: false,
66
+ defaultValue: {},
67
+ },
68
+ isEnabled: {
69
+ type: sequelize_1.DataTypes.BOOLEAN,
70
+ allowNull: false,
71
+ defaultValue: true,
72
+ field: 'is_enabled',
73
+ },
74
+ lastConnectedAt: {
75
+ type: sequelize_1.DataTypes.DATE,
76
+ allowNull: true,
77
+ field: 'last_connected_at',
78
+ },
79
+ lastError: {
80
+ type: sequelize_1.DataTypes.TEXT,
81
+ allowNull: true,
82
+ field: 'last_error',
83
+ },
84
+ }, {
85
+ sequelize: db_1.sequelize,
86
+ tableName: 'mcp_servers',
87
+ modelName: 'MCPServer',
88
+ indexes: [
89
+ {
90
+ unique: true,
91
+ fields: ['subscription_id', 'name'],
92
+ },
93
+ ],
94
+ });
95
+ subscription_1.Subscription.hasMany(MCPServer, {
96
+ foreignKey: 'subscriptionId',
97
+ as: 'mcpServers',
98
+ });
99
+ MCPServer.belongsTo(subscription_1.Subscription, {
100
+ foreignKey: 'subscriptionId',
101
+ as: 'subscription',
102
+ });