@vellumai/assistant 0.3.20 → 0.3.21

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,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
+ }
@@ -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 !== 'skill';
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
  */
@@ -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 or provided by a skill. */
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). */