@vellumai/assistant 0.3.20 → 0.3.22
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/bun.lock +139 -2
- package/package.json +2 -1
- package/src/__tests__/mcp-cli.test.ts +141 -0
- package/src/cli/mcp.ts +130 -0
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/schema.ts +12 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +5 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +1 -0
- package/src/daemon/lifecycle.ts +8 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/index.ts +2 -0
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/registry.ts +64 -1
- package/src/tools/types.ts +4 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { McpConfig, McpServerConfig } from '../config/mcp-schema.js';
|
|
2
|
+
import { getLogger } from '../util/logger.js';
|
|
3
|
+
import { McpClient, type McpToolInfo } from './client.js';
|
|
4
|
+
|
|
5
|
+
const log = getLogger('mcp-manager');
|
|
6
|
+
|
|
7
|
+
export interface McpServerToolInfo {
|
|
8
|
+
serverId: string;
|
|
9
|
+
serverConfig: McpServerConfig;
|
|
10
|
+
tools: McpToolInfo[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class McpServerManager {
|
|
14
|
+
private clients = new Map<string, McpClient>();
|
|
15
|
+
private serverConfigs = new Map<string, McpServerConfig>();
|
|
16
|
+
|
|
17
|
+
async start(config: McpConfig): Promise<McpServerToolInfo[]> {
|
|
18
|
+
const results: McpServerToolInfo[] = [];
|
|
19
|
+
|
|
20
|
+
console.log(`[MCP] Starting ${Object.keys(config.servers).length} server(s)...`);
|
|
21
|
+
for (const [serverId, serverConfig] of Object.entries(config.servers)) {
|
|
22
|
+
if (!serverConfig.enabled) {
|
|
23
|
+
console.log(`[MCP] Server "${serverId}" is disabled, skipping`);
|
|
24
|
+
log.info({ serverId }, 'MCP server disabled, skipping');
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
console.log(`[MCP] Starting server "${serverId}" (transport: ${serverConfig.transport.type})`);
|
|
30
|
+
const client = new McpClient(serverId);
|
|
31
|
+
await client.connect(serverConfig.transport);
|
|
32
|
+
this.clients.set(serverId, client);
|
|
33
|
+
this.serverConfigs.set(serverId, serverConfig);
|
|
34
|
+
|
|
35
|
+
let tools = await client.listTools();
|
|
36
|
+
log.info({ serverId, toolCount: tools.length }, 'MCP server tools discovered');
|
|
37
|
+
|
|
38
|
+
// Apply tool filtering
|
|
39
|
+
tools = this.filterTools(tools, serverConfig);
|
|
40
|
+
|
|
41
|
+
// Apply per-server maxTools limit
|
|
42
|
+
if (tools.length > serverConfig.maxTools) {
|
|
43
|
+
log.warn(
|
|
44
|
+
{ serverId, discovered: tools.length, max: serverConfig.maxTools },
|
|
45
|
+
'MCP server exceeded maxTools limit, truncating',
|
|
46
|
+
);
|
|
47
|
+
tools = tools.slice(0, serverConfig.maxTools);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
results.push({ serverId, serverConfig, tools });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`[MCP] Failed to connect to server "${serverId}":`, err);
|
|
53
|
+
log.error({ err, serverId }, 'Failed to connect to MCP server');
|
|
54
|
+
// Clean up any partially-connected client
|
|
55
|
+
const staleClient = this.clients.get(serverId);
|
|
56
|
+
if (staleClient) {
|
|
57
|
+
try { await staleClient.disconnect(); } catch { /* ignore */ }
|
|
58
|
+
this.clients.delete(serverId);
|
|
59
|
+
this.serverConfigs.delete(serverId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Apply global max tools limit
|
|
65
|
+
const totalTools = results.reduce((sum, r) => sum + r.tools.length, 0);
|
|
66
|
+
if (totalTools > config.globalMaxTools) {
|
|
67
|
+
log.warn(
|
|
68
|
+
{ totalTools, globalMax: config.globalMaxTools },
|
|
69
|
+
'Total MCP tools exceed globalMaxTools, truncating',
|
|
70
|
+
);
|
|
71
|
+
let remaining = config.globalMaxTools;
|
|
72
|
+
for (const result of results) {
|
|
73
|
+
if (remaining <= 0) {
|
|
74
|
+
result.tools = [];
|
|
75
|
+
} else if (result.tools.length > remaining) {
|
|
76
|
+
result.tools = result.tools.slice(0, remaining);
|
|
77
|
+
}
|
|
78
|
+
remaining -= result.tools.length;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async stop(): Promise<void> {
|
|
86
|
+
const disconnects = Array.from(this.clients.values()).map((client) =>
|
|
87
|
+
client.disconnect().catch((err) => {
|
|
88
|
+
log.warn({ err, serverId: client.serverId }, 'Error disconnecting MCP server');
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
await Promise.all(disconnects);
|
|
92
|
+
this.clients.clear();
|
|
93
|
+
this.serverConfigs.clear();
|
|
94
|
+
log.info('All MCP servers disconnected');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async callTool(serverId: string, toolName: string, args: Record<string, unknown>) {
|
|
98
|
+
const client = this.clients.get(serverId);
|
|
99
|
+
if (!client) {
|
|
100
|
+
throw new Error(`MCP server "${serverId}" not found`);
|
|
101
|
+
}
|
|
102
|
+
return client.callTool(toolName, args);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getClient(serverId: string): McpClient | undefined {
|
|
106
|
+
return this.clients.get(serverId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private filterTools(tools: McpToolInfo[], config: McpServerConfig): McpToolInfo[] {
|
|
110
|
+
let filtered = tools;
|
|
111
|
+
|
|
112
|
+
if (config.allowedTools) {
|
|
113
|
+
const allowed = new Set(config.allowedTools);
|
|
114
|
+
filtered = filtered.filter((t) => allowed.has(t.name));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (config.blockedTools) {
|
|
118
|
+
const blocked = new Set(config.blockedTools);
|
|
119
|
+
filtered = filtered.filter((t) => !blocked.has(t.name));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return filtered;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Singleton instance
|
|
127
|
+
let instance: McpServerManager | null = null;
|
|
128
|
+
|
|
129
|
+
export function getMcpServerManager(): McpServerManager {
|
|
130
|
+
if (!instance) {
|
|
131
|
+
instance = new McpServerManager();
|
|
132
|
+
}
|
|
133
|
+
return instance;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Reset singleton for testing. */
|
|
137
|
+
export function __resetMcpManagerForTesting(): void {
|
|
138
|
+
instance = null;
|
|
139
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync, statfsSync,statSync } from 'node:fs';
|
|
6
|
+
import { cpus, totalmem } from 'node:os';
|
|
6
7
|
import { dirname,join } from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
@@ -36,6 +37,76 @@ function getDiskSpaceInfo(): DiskSpaceInfo | null {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface MemoryInfo {
|
|
41
|
+
currentMb: number;
|
|
42
|
+
maxMb: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read the container memory limit from cgroups if available, falling back to host total.
|
|
46
|
+
// cgroups v2: /sys/fs/cgroup/memory.max (returns "max" when unlimited)
|
|
47
|
+
// cgroups v1: /sys/fs/cgroup/memory/memory.limit_in_bytes (large sentinel when unlimited)
|
|
48
|
+
function getContainerMemoryLimitBytes(): number | null {
|
|
49
|
+
try {
|
|
50
|
+
const v2 = readFileSync('/sys/fs/cgroup/memory.max', 'utf-8').trim();
|
|
51
|
+
if (v2 !== 'max') {
|
|
52
|
+
const bytes = parseInt(v2, 10);
|
|
53
|
+
if (!isNaN(bytes) && bytes > 0) return bytes;
|
|
54
|
+
}
|
|
55
|
+
} catch { /* not available */ }
|
|
56
|
+
try {
|
|
57
|
+
const v1 = readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf-8').trim();
|
|
58
|
+
const bytes = parseInt(v1, 10);
|
|
59
|
+
// cgroups v1 uses a near-INT64_MAX sentinel when no limit is set
|
|
60
|
+
if (!isNaN(bytes) && bytes > 0 && bytes < totalmem() * 1.5) return bytes;
|
|
61
|
+
} catch { /* not available */ }
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getMemoryInfo(): MemoryInfo {
|
|
66
|
+
const bytesToMb = (b: number) => Math.round((b / (1024 * 1024)) * 100) / 100;
|
|
67
|
+
return {
|
|
68
|
+
currentMb: bytesToMb(process.memoryUsage().rss),
|
|
69
|
+
maxMb: bytesToMb(getContainerMemoryLimitBytes() ?? totalmem()),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CpuInfo {
|
|
74
|
+
currentPercent: number;
|
|
75
|
+
maxCores: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Track CPU usage over a rolling window so /healthz reports near-real-time
|
|
79
|
+
// utilization instead of a lifetime average (total CPU time / total uptime).
|
|
80
|
+
const CPU_SAMPLE_INTERVAL_MS = 5_000;
|
|
81
|
+
let _lastCpuUsage: NodeJS.CpuUsage = process.cpuUsage();
|
|
82
|
+
let _lastCpuTime: number = Date.now();
|
|
83
|
+
let _cachedCpuPercent = 0;
|
|
84
|
+
|
|
85
|
+
// Kick off the background sampler. unref() so it never prevents process exit.
|
|
86
|
+
setInterval(() => {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const newUsage = process.cpuUsage();
|
|
89
|
+
const elapsedMs = now - _lastCpuTime;
|
|
90
|
+
if (elapsedMs > 0) {
|
|
91
|
+
const deltaCpuUs =
|
|
92
|
+
(newUsage.user - _lastCpuUsage.user) +
|
|
93
|
+
(newUsage.system - _lastCpuUsage.system);
|
|
94
|
+
const deltaCpuMs = deltaCpuUs / 1000;
|
|
95
|
+
const numCores = cpus().length;
|
|
96
|
+
_cachedCpuPercent =
|
|
97
|
+
Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
|
|
98
|
+
}
|
|
99
|
+
_lastCpuUsage = newUsage;
|
|
100
|
+
_lastCpuTime = now;
|
|
101
|
+
}, CPU_SAMPLE_INTERVAL_MS).unref();
|
|
102
|
+
|
|
103
|
+
function getCpuInfo(): CpuInfo {
|
|
104
|
+
return {
|
|
105
|
+
currentPercent: _cachedCpuPercent,
|
|
106
|
+
maxCores: cpus().length,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
39
110
|
function getPackageVersion(): string | undefined {
|
|
40
111
|
try {
|
|
41
112
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
@@ -52,6 +123,8 @@ export function handleHealth(): Response {
|
|
|
52
123
|
timestamp: new Date().toISOString(),
|
|
53
124
|
version: getPackageVersion(),
|
|
54
125
|
disk: getDiskSpaceInfo(),
|
|
126
|
+
memory: getMemoryInfo(),
|
|
127
|
+
cpu: getCpuInfo(),
|
|
55
128
|
});
|
|
56
129
|
}
|
|
57
130
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { McpServerConfig } from '../../config/mcp-schema.js';
|
|
2
|
+
import type { McpServerManager } from '../../mcp/manager.js';
|
|
3
|
+
import { RiskLevel } from '../../permissions/types.js';
|
|
4
|
+
import type { ToolDefinition } from '../../providers/types.js';
|
|
5
|
+
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
6
|
+
|
|
7
|
+
const riskMap: Record<string, RiskLevel> = {
|
|
8
|
+
low: RiskLevel.Low,
|
|
9
|
+
medium: RiskLevel.Medium,
|
|
10
|
+
high: RiskLevel.High,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a namespaced tool name to prevent collisions across MCP servers
|
|
15
|
+
* and with core/skill tools.
|
|
16
|
+
*/
|
|
17
|
+
export function mcpToolName(serverId: string, toolName: string): string {
|
|
18
|
+
return `mcp__${serverId}__${toolName}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a namespaced MCP tool name back into serverId and original tool name.
|
|
23
|
+
* Returns null if the name doesn't match the MCP naming convention.
|
|
24
|
+
*/
|
|
25
|
+
export function parseMcpToolName(name: string): { serverId: string; toolName: string } | null {
|
|
26
|
+
if (!name.startsWith('mcp__')) return null;
|
|
27
|
+
const prefixRemoved = name.slice(5); // remove 'mcp__'
|
|
28
|
+
const firstSep = prefixRemoved.indexOf('__');
|
|
29
|
+
if (firstSep === -1) return null;
|
|
30
|
+
return {
|
|
31
|
+
serverId: prefixRemoved.slice(0, firstSep),
|
|
32
|
+
toolName: prefixRemoved.slice(firstSep + 2),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface McpToolMetadata {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
inputSchema: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a Tool object from MCP tool metadata.
|
|
44
|
+
* The tool delegates execution to the McpServerManager.
|
|
45
|
+
*/
|
|
46
|
+
export function createMcpTool(
|
|
47
|
+
metadata: McpToolMetadata,
|
|
48
|
+
serverId: string,
|
|
49
|
+
serverConfig: McpServerConfig,
|
|
50
|
+
manager: McpServerManager,
|
|
51
|
+
): Tool {
|
|
52
|
+
const namespacedName = mcpToolName(serverId, metadata.name);
|
|
53
|
+
const riskLevel = riskMap[serverConfig.defaultRiskLevel] ?? RiskLevel.High;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: namespacedName,
|
|
57
|
+
description: metadata.description,
|
|
58
|
+
category: 'mcp',
|
|
59
|
+
defaultRiskLevel: riskLevel,
|
|
60
|
+
origin: 'mcp',
|
|
61
|
+
ownerMcpServerId: serverId,
|
|
62
|
+
executionTarget: 'host',
|
|
63
|
+
|
|
64
|
+
getDefinition(): ToolDefinition {
|
|
65
|
+
return {
|
|
66
|
+
name: namespacedName,
|
|
67
|
+
description: metadata.description,
|
|
68
|
+
input_schema: metadata.inputSchema as ToolDefinition['input_schema'],
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
|
|
73
|
+
try {
|
|
74
|
+
const result = await manager.callTool(serverId, metadata.name, input);
|
|
75
|
+
return {
|
|
76
|
+
content: result.content,
|
|
77
|
+
isError: result.isError,
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
return {
|
|
82
|
+
content: `MCP tool execution failed: ${message}`,
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create Tool objects from all tools provided by an MCP server.
|
|
92
|
+
*/
|
|
93
|
+
export function createMcpToolsFromServer(
|
|
94
|
+
tools: McpToolMetadata[],
|
|
95
|
+
serverId: string,
|
|
96
|
+
serverConfig: McpServerConfig,
|
|
97
|
+
manager: McpServerManager,
|
|
98
|
+
): Tool[] {
|
|
99
|
+
return tools.map((tool) => createMcpTool(tool, serverId, serverConfig, manager));
|
|
100
|
+
}
|
package/src/tools/registry.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function registerSkillTools(newTools: Tool[]): Tool[] {
|
|
|
121
121
|
for (const tool of newTools) {
|
|
122
122
|
const existing = tools.get(tool.name);
|
|
123
123
|
if (existing) {
|
|
124
|
-
const existingIsCore = existing.origin
|
|
124
|
+
const existingIsCore = existing.origin === 'core' || !existing.origin;
|
|
125
125
|
if (existingIsCore) {
|
|
126
126
|
log.warn(
|
|
127
127
|
{ toolName: tool.name, skillId: tool.ownerSkillId },
|
|
@@ -176,6 +176,69 @@ export function unregisterSkillTools(skillId: string): void {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Register multiple MCP-origin tools at once.
|
|
181
|
+
* Skips any tool whose name collides with a core tool (logs a warning).
|
|
182
|
+
* Throws if a tool name collides with a tool owned by a different MCP server.
|
|
183
|
+
*/
|
|
184
|
+
export function registerMcpTools(newTools: Tool[]): Tool[] {
|
|
185
|
+
const accepted: Tool[] = [];
|
|
186
|
+
for (const tool of newTools) {
|
|
187
|
+
const existing = tools.get(tool.name);
|
|
188
|
+
if (existing) {
|
|
189
|
+
const existingIsCore = existing.origin === 'core' || !existing.origin;
|
|
190
|
+
if (existingIsCore) {
|
|
191
|
+
log.warn(
|
|
192
|
+
{ toolName: tool.name, serverId: tool.ownerMcpServerId },
|
|
193
|
+
`MCP server "${tool.ownerMcpServerId}" tried to register tool "${tool.name}" which conflicts with a core tool. Skipping.`,
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (existing.origin === 'skill') {
|
|
198
|
+
log.warn(
|
|
199
|
+
{ toolName: tool.name, serverId: tool.ownerMcpServerId, skillId: existing.ownerSkillId },
|
|
200
|
+
`MCP server "${tool.ownerMcpServerId}" tried to register tool "${tool.name}" which conflicts with skill tool from "${existing.ownerSkillId}". Skipping.`,
|
|
201
|
+
);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (existing.origin === 'mcp' && existing.ownerMcpServerId !== tool.ownerMcpServerId) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`MCP tool "${tool.name}" is already registered by MCP server "${existing.ownerMcpServerId}"`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
accepted.push(tool);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const tool of accepted) {
|
|
214
|
+
tools.set(tool.name, tool);
|
|
215
|
+
log.info({ name: tool.name, ownerMcpServerId: tool.ownerMcpServerId }, 'MCP tool registered');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return accepted;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Unregister all tools belonging to a specific MCP server.
|
|
223
|
+
*/
|
|
224
|
+
export function unregisterMcpTools(serverId: string): void {
|
|
225
|
+
for (const [name, tool] of tools) {
|
|
226
|
+
if (tool.origin === 'mcp' && tool.ownerMcpServerId === serverId) {
|
|
227
|
+
tools.delete(name);
|
|
228
|
+
log.info({ name, serverId }, 'MCP tool unregistered');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Return the names of all currently registered MCP-origin tools.
|
|
235
|
+
*/
|
|
236
|
+
export function getMcpToolNames(): string[] {
|
|
237
|
+
return Array.from(tools.values())
|
|
238
|
+
.filter((t) => t.origin === 'mcp')
|
|
239
|
+
.map((t) => t.name);
|
|
240
|
+
}
|
|
241
|
+
|
|
179
242
|
/**
|
|
180
243
|
* Return the names of all currently registered skill-origin tools.
|
|
181
244
|
*/
|
package/src/tools/types.ts
CHANGED
|
@@ -170,10 +170,12 @@ export interface Tool {
|
|
|
170
170
|
defaultRiskLevel: RiskLevel;
|
|
171
171
|
/** When set to 'proxy', the tool is forwarded to a connected client rather than executed locally. */
|
|
172
172
|
executionMode?: 'local' | 'proxy';
|
|
173
|
-
/** Whether this tool is a core built-in
|
|
174
|
-
origin?: 'core' | 'skill';
|
|
173
|
+
/** Whether this tool is a core built-in, provided by a skill, or from an MCP server. */
|
|
174
|
+
origin?: 'core' | 'skill' | 'mcp';
|
|
175
175
|
/** If origin is 'skill', the ID of the owning skill. */
|
|
176
176
|
ownerSkillId?: string;
|
|
177
|
+
/** If origin is 'mcp', the ID of the owning MCP server. */
|
|
178
|
+
ownerMcpServerId?: string;
|
|
177
179
|
/** Content-hash of the owning skill's source at registration time. */
|
|
178
180
|
ownerSkillVersionHash?: string;
|
|
179
181
|
/** Whether the owning skill is bundled with the daemon (trusted first-party). */
|