@objectstack/plugin-mcp-server 4.0.2

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,495 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import type { Logger, IMetadataService, IDataEngine, AIToolDefinition } from '@objectstack/spec/contracts';
6
+ import type { Agent } from '@objectstack/spec';
7
+ import type { ToolRegistry, ToolExecutionResult } from './types.js';
8
+ import { z } from 'zod';
9
+
10
+ /**
11
+ * Configuration for the MCP Server Runtime.
12
+ */
13
+ export interface MCPServerRuntimeConfig {
14
+ /** Human-readable server name. */
15
+ name?: string;
16
+ /** Server version (semver). */
17
+ version?: string;
18
+ /** Optional instructions describing how to use the server. */
19
+ instructions?: string;
20
+ /** Transport mode: 'stdio' (default) or 'http'. */
21
+ transport?: 'stdio' | 'http';
22
+ /** Logger instance. */
23
+ logger?: Logger;
24
+ }
25
+
26
+ /**
27
+ * Minimal shape of an object definition returned by IMetadataService.
28
+ */
29
+ interface ObjectDef {
30
+ name: string;
31
+ label?: string;
32
+ fields?: Record<string, { name?: string; type?: string; label?: string; required?: boolean }>;
33
+ enable?: Record<string, boolean>;
34
+ }
35
+
36
+ /**
37
+ * Names of tools that are read-only (no side effects).
38
+ * Kept as a module-level constant for easy extension.
39
+ */
40
+ const READ_ONLY_TOOLS = new Set([
41
+ 'list_objects',
42
+ 'describe_object',
43
+ 'query_records',
44
+ 'get_record',
45
+ 'aggregate_data',
46
+ ]);
47
+
48
+ /**
49
+ * Names of tools that perform destructive mutations.
50
+ */
51
+ const DESTRUCTIVE_TOOLS = new Set([
52
+ 'delete_field',
53
+ ]);
54
+
55
+ /**
56
+ * MCPServerRuntime — Bridges ObjectStack kernel services to the Model Context Protocol.
57
+ *
58
+ * Responsibilities:
59
+ * 1. Bridge ToolRegistry → MCP tools (all registered AI tools)
60
+ * 2. Bridge IMetadataService → MCP resources (object schemas, metadata types)
61
+ * 3. Bridge IDataEngine → MCP resources (record access by URI)
62
+ * 4. Bridge Agent definitions → MCP prompts (agent instructions)
63
+ *
64
+ * Architecture:
65
+ * ```
66
+ * ToolRegistry (service-ai) ──┐
67
+ * IMetadataService (metadata) ─┼──→ MCPServerRuntime ──→ McpServer (SDK)
68
+ * IDataEngine (objectql) ──┤ │
69
+ * Agent definitions ──┘ ├── stdio transport
70
+ * └── http transport (future)
71
+ * ```
72
+ */
73
+ export class MCPServerRuntime {
74
+ private readonly mcpServer: McpServer;
75
+ private readonly config: Required<Pick<MCPServerRuntimeConfig, 'name' | 'version'>> & MCPServerRuntimeConfig;
76
+ private transport: StdioServerTransport | undefined;
77
+ private started = false;
78
+
79
+ constructor(config: MCPServerRuntimeConfig = {}) {
80
+ this.config = {
81
+ name: 'objectstack',
82
+ version: '1.0.0',
83
+ transport: 'stdio',
84
+ ...config,
85
+ };
86
+
87
+ this.mcpServer = new McpServer(
88
+ {
89
+ name: this.config.name,
90
+ version: this.config.version,
91
+ },
92
+ {
93
+ capabilities: {
94
+ resources: {},
95
+ tools: {},
96
+ prompts: {},
97
+ logging: {},
98
+ },
99
+ instructions: this.config.instructions ?? 'ObjectStack MCP Server — access data objects, AI tools, and agent prompts.',
100
+ },
101
+ );
102
+ }
103
+
104
+ /** The underlying McpServer instance (for advanced use cases). */
105
+ get server(): McpServer {
106
+ return this.mcpServer;
107
+ }
108
+
109
+ /** Whether the server is currently connected and running. */
110
+ get isStarted(): boolean {
111
+ return this.started;
112
+ }
113
+
114
+ // ── Helpers ─────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Extract the text value from a ToolExecutionResult's output.
118
+ *
119
+ * The output may be a `{ type: 'text', value: string }` object (from the
120
+ * Vercel AI SDK ToolResultPart) or any serialisable value.
121
+ */
122
+ private static formatToolOutput(result: ToolExecutionResult): string {
123
+ const output = result.output;
124
+ if (output && typeof output === 'object' && 'value' in output) {
125
+ return String((output as { value: unknown }).value);
126
+ }
127
+ return JSON.stringify(output ?? '');
128
+ }
129
+
130
+ // ── Tool Bridge ────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Bridge all tools from the ToolRegistry to MCP tools.
134
+ *
135
+ * Each registered tool becomes an MCP tool with the same name, description,
136
+ * and JSON Schema parameters. The handler delegates to the ToolRegistry's
137
+ * execute path.
138
+ */
139
+ bridgeTools(toolRegistry: ToolRegistry): void {
140
+ const tools = toolRegistry.getAll();
141
+ const logger = this.config.logger;
142
+
143
+ for (const tool of tools) {
144
+ this.registerToolFromDefinition(tool, toolRegistry);
145
+ }
146
+
147
+ logger?.info(`[MCP] Bridged ${tools.length} tools from ToolRegistry`);
148
+ }
149
+
150
+ /**
151
+ * Register a single tool on the MCP server from an AIToolDefinition.
152
+ */
153
+ private registerToolFromDefinition(tool: AIToolDefinition, toolRegistry: ToolRegistry): void {
154
+ const logger = this.config.logger;
155
+
156
+ // Convert JSON Schema parameters to Zod-compatible format for MCP SDK
157
+ // The MCP SDK registerTool with inputSchema expects a Zod raw shape or AnySchema.
158
+ // Since our tools use JSON Schema, we use the low-level .tool() with a raw callback
159
+ // and pass the JSON Schema as annotations metadata.
160
+ this.mcpServer.registerTool(
161
+ tool.name,
162
+ {
163
+ description: tool.description,
164
+ annotations: {
165
+ // Mark tools with write side-effects for destructive operations
166
+ destructiveHint: this.isDestructiveTool(tool.name),
167
+ readOnlyHint: this.isReadOnlyTool(tool.name),
168
+ openWorldHint: false,
169
+ },
170
+ },
171
+ async (extra) => {
172
+ // The MCP SDK passes tool arguments via the extra.arguments property
173
+ // when registerTool is called without an inputSchema.
174
+ const rawExtra = extra as Record<string, unknown>;
175
+ const args = (rawExtra.arguments ?? {}) as Record<string, unknown>;
176
+
177
+ try {
178
+ const result = await toolRegistry.execute({
179
+ type: 'tool-call',
180
+ toolCallId: `mcp-${tool.name}-${Date.now()}`,
181
+ toolName: tool.name,
182
+ input: args,
183
+ });
184
+
185
+ const outputText = MCPServerRuntime.formatToolOutput(result);
186
+
187
+ if (result.isError) {
188
+ return {
189
+ content: [{ type: 'text' as const, text: outputText }],
190
+ isError: true,
191
+ };
192
+ }
193
+
194
+ return {
195
+ content: [{ type: 'text' as const, text: outputText }],
196
+ };
197
+ } catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ logger?.warn(`[MCP] Tool "${tool.name}" execution failed:`, { error: message });
200
+ return {
201
+ content: [{ type: 'text' as const, text: message }],
202
+ isError: true,
203
+ };
204
+ }
205
+ },
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Check if a tool is read-only (data query tools).
211
+ */
212
+ private isReadOnlyTool(name: string): boolean {
213
+ return READ_ONLY_TOOLS.has(name);
214
+ }
215
+
216
+ /**
217
+ * Check if a tool performs destructive operations.
218
+ */
219
+ private isDestructiveTool(name: string): boolean {
220
+ return DESTRUCTIVE_TOOLS.has(name);
221
+ }
222
+
223
+ // ── Resource Bridge ────────────────────────────────────────────
224
+
225
+ /**
226
+ * Bridge metadata service and data engine to MCP resources.
227
+ *
228
+ * Exposes:
229
+ * - `objectstack://objects` — List all data objects
230
+ * - `objectstack://objects/{objectName}` — Get object schema
231
+ * - `objectstack://objects/{objectName}/records/{recordId}` — Get a specific record
232
+ * - `objectstack://metadata/types` — List all metadata types
233
+ */
234
+ bridgeResources(metadataService: IMetadataService, dataEngine?: IDataEngine): void {
235
+ const logger = this.config.logger;
236
+ let resourceCount = 0;
237
+
238
+ // ── Static resource: List all objects ──
239
+ this.mcpServer.registerResource(
240
+ 'object_list',
241
+ 'objectstack://objects',
242
+ {
243
+ description: 'List all data objects (tables) in the ObjectStack instance',
244
+ mimeType: 'application/json',
245
+ },
246
+ async () => {
247
+ const objects = await metadataService.listObjects();
248
+ const summary = (objects as ObjectDef[]).map(o => ({
249
+ name: o.name,
250
+ label: o.label ?? o.name,
251
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0,
252
+ }));
253
+
254
+ return {
255
+ contents: [{
256
+ uri: 'objectstack://objects',
257
+ mimeType: 'application/json',
258
+ text: JSON.stringify({ objects: summary, totalCount: summary.length }, null, 2),
259
+ }],
260
+ };
261
+ },
262
+ );
263
+ resourceCount++;
264
+
265
+ // ── Template resource: Object schema ──
266
+ this.mcpServer.registerResource(
267
+ 'object_schema',
268
+ new ResourceTemplate('objectstack://objects/{objectName}', { list: undefined }),
269
+ {
270
+ description: 'Get the full schema of a specific data object including fields and features',
271
+ mimeType: 'application/json',
272
+ },
273
+ async (_uri, variables) => {
274
+ const objectName = String(variables.objectName);
275
+ const objectDef = await metadataService.getObject(objectName);
276
+
277
+ if (!objectDef) {
278
+ return {
279
+ contents: [{
280
+ uri: `objectstack://objects/${objectName}`,
281
+ mimeType: 'application/json',
282
+ text: JSON.stringify({ error: `Object "${objectName}" not found` }),
283
+ }],
284
+ };
285
+ }
286
+
287
+ const def = objectDef as ObjectDef;
288
+ const fields = def.fields ?? {};
289
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
290
+ name: key,
291
+ type: f.type,
292
+ label: f.label ?? key,
293
+ required: f.required ?? false,
294
+ }));
295
+
296
+ return {
297
+ contents: [{
298
+ uri: `objectstack://objects/${objectName}`,
299
+ mimeType: 'application/json',
300
+ text: JSON.stringify({
301
+ name: def.name,
302
+ label: def.label ?? def.name,
303
+ fields: fieldSummary,
304
+ enableFeatures: def.enable ?? {},
305
+ }, null, 2),
306
+ }],
307
+ };
308
+ },
309
+ );
310
+ resourceCount++;
311
+
312
+ // ── Template resource: Record by ID ──
313
+ if (dataEngine) {
314
+ this.mcpServer.registerResource(
315
+ 'record_by_id',
316
+ new ResourceTemplate('objectstack://objects/{objectName}/records/{recordId}', { list: undefined }),
317
+ {
318
+ description: 'Get a specific record by ID from a data object',
319
+ mimeType: 'application/json',
320
+ },
321
+ async (_uri, variables) => {
322
+ const objectName = String(variables.objectName);
323
+ const recordId = String(variables.recordId);
324
+
325
+ try {
326
+ const record = await dataEngine.findOne(objectName, {
327
+ where: { id: recordId },
328
+ });
329
+
330
+ if (!record) {
331
+ return {
332
+ contents: [{
333
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
334
+ mimeType: 'application/json',
335
+ text: JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` }),
336
+ }],
337
+ };
338
+ }
339
+
340
+ return {
341
+ contents: [{
342
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
343
+ mimeType: 'application/json',
344
+ text: JSON.stringify(record, null, 2),
345
+ }],
346
+ };
347
+ } catch (err) {
348
+ const message = err instanceof Error ? err.message : String(err);
349
+ return {
350
+ contents: [{
351
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
352
+ mimeType: 'application/json',
353
+ text: JSON.stringify({ error: message }),
354
+ }],
355
+ };
356
+ }
357
+ },
358
+ );
359
+ resourceCount++;
360
+ }
361
+
362
+ // ── Static resource: Metadata types ──
363
+ if (metadataService.getRegisteredTypes) {
364
+ this.mcpServer.registerResource(
365
+ 'metadata_types',
366
+ 'objectstack://metadata/types',
367
+ {
368
+ description: 'List all registered metadata types (object, app, view, agent, tool, etc.)',
369
+ mimeType: 'application/json',
370
+ },
371
+ async () => {
372
+ const types = await metadataService.getRegisteredTypes!();
373
+ return {
374
+ contents: [{
375
+ uri: 'objectstack://metadata/types',
376
+ mimeType: 'application/json',
377
+ text: JSON.stringify({ types, totalCount: types.length }, null, 2),
378
+ }],
379
+ };
380
+ },
381
+ );
382
+ resourceCount++;
383
+ }
384
+
385
+ logger?.info(`[MCP] Bridged ${resourceCount} resource endpoints`);
386
+ }
387
+
388
+ // ── Prompt Bridge ──────────────────────────────────────────────
389
+
390
+ /**
391
+ * Bridge registered agents to MCP prompts.
392
+ *
393
+ * Each active agent becomes an MCP prompt with:
394
+ * - Name matching the agent name
395
+ * - System message from agent instructions
396
+ * - Optional context arguments (objectName, recordId, viewName)
397
+ */
398
+ bridgePrompts(metadataService: IMetadataService): void {
399
+ const logger = this.config.logger;
400
+
401
+ // Register a dynamic prompt that loads agents at call time
402
+ this.mcpServer.registerPrompt(
403
+ 'agent_prompt',
404
+ {
405
+ description: 'Load an agent\'s system prompt with optional UI context. ' +
406
+ 'Use the agentName argument to select which agent\'s instructions to use.',
407
+ argsSchema: {
408
+ agentName: z.string().describe('Name of the agent to load (e.g. "data_chat", "metadata_assistant")'),
409
+ objectName: z.string().optional().describe('Current object the user is viewing'),
410
+ recordId: z.string().optional().describe('Currently selected record ID'),
411
+ viewName: z.string().optional().describe('Current view name'),
412
+ },
413
+ },
414
+ async (args) => {
415
+ const agentName = String(args.agentName ?? '');
416
+ if (!agentName) {
417
+ return {
418
+ messages: [{
419
+ role: 'user' as const,
420
+ content: { type: 'text' as const, text: 'Error: agentName argument is required' },
421
+ }],
422
+ };
423
+ }
424
+
425
+ const raw = await metadataService.get('agent', agentName);
426
+ if (!raw) {
427
+ return {
428
+ messages: [{
429
+ role: 'user' as const,
430
+ content: { type: 'text' as const, text: `Error: Agent "${agentName}" not found` },
431
+ }],
432
+ };
433
+ }
434
+
435
+ const agent = raw as Agent;
436
+
437
+ // Build system prompt from agent instructions + context
438
+ const parts: string[] = [];
439
+ parts.push(agent.instructions ?? '');
440
+
441
+ const contextHints: string[] = [];
442
+ if (args.objectName) contextHints.push(`Current object: ${args.objectName}`);
443
+ if (args.recordId) contextHints.push(`Selected record ID: ${args.recordId}`);
444
+ if (args.viewName) contextHints.push(`Current view: ${args.viewName}`);
445
+ if (contextHints.length > 0) {
446
+ parts.push('\n--- Current Context ---\n' + contextHints.join('\n'));
447
+ }
448
+
449
+ return {
450
+ messages: [{
451
+ role: 'assistant' as const,
452
+ content: { type: 'text' as const, text: parts.join('\n') },
453
+ }],
454
+ };
455
+ },
456
+ );
457
+
458
+ logger?.info('[MCP] Agent prompts bridged');
459
+ }
460
+
461
+ // ── Lifecycle ──────────────────────────────────────────────────
462
+
463
+ /**
464
+ * Start the MCP server with the configured transport.
465
+ *
466
+ * For stdio transport, this connects to process stdin/stdout.
467
+ */
468
+ async start(): Promise<void> {
469
+ if (this.started) return;
470
+
471
+ const logger = this.config.logger;
472
+
473
+ if (this.config.transport === 'stdio') {
474
+ this.transport = new StdioServerTransport();
475
+ await this.mcpServer.connect(this.transport);
476
+ this.started = true;
477
+ logger?.info(`[MCP] Server started (transport: stdio, name: ${this.config.name})`);
478
+ } else {
479
+ // HTTP transport support will be added in a future version
480
+ logger?.warn('[MCP] HTTP transport is not yet supported. Use stdio transport.');
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Stop the MCP server and disconnect the transport.
486
+ */
487
+ async stop(): Promise<void> {
488
+ if (!this.started) return;
489
+
490
+ await this.mcpServer.close();
491
+ this.transport = undefined;
492
+ this.started = false;
493
+ this.config.logger?.info('[MCP] Server stopped');
494
+ }
495
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,139 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { Plugin, PluginContext } from '@objectstack/core';
4
+ import type { IAIService, IDataEngine, IMetadataService } from '@objectstack/spec/contracts';
5
+ import { MCPServerRuntime } from './mcp-server-runtime.js';
6
+ import type { MCPServerRuntimeConfig } from './mcp-server-runtime.js';
7
+ import type { ToolRegistry } from './types.js';
8
+
9
+ /**
10
+ * Configuration options for the MCPServerPlugin.
11
+ */
12
+ export interface MCPServerPluginOptions {
13
+ /** Override MCP server name. Defaults to 'objectstack'. */
14
+ name?: string;
15
+ /** Override MCP server version. Defaults to package version. */
16
+ version?: string;
17
+ /** Transport mode: 'stdio' (default). */
18
+ transport?: 'stdio' | 'http';
19
+ /** Whether to auto-start the MCP server. Defaults to false (manual start via env var). */
20
+ autoStart?: boolean;
21
+ /** Custom instructions for the MCP server. */
22
+ instructions?: string;
23
+ }
24
+
25
+ /**
26
+ * MCPServerPlugin — Kernel plugin that exposes ObjectStack as an MCP server.
27
+ *
28
+ * Lifecycle:
29
+ * 1. **init** — Creates {@link MCPServerRuntime} and registers as `'mcp'` service.
30
+ * 2. **start** — Bridges ToolRegistry, MetadataService, DataEngine, and Agents
31
+ * to the MCP server. Starts the transport if `autoStart` is enabled or
32
+ * the `MCP_SERVER_ENABLED` environment variable is set.
33
+ * 3. **destroy** — Stops the MCP transport.
34
+ *
35
+ * Environment Variables:
36
+ * - `MCP_SERVER_ENABLED=true` — Enable MCP server at startup
37
+ * - `MCP_SERVER_NAME` — Override server name
38
+ * - `MCP_SERVER_TRANSPORT` — Override transport ('stdio' | 'http')
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { LiteKernel } from '@objectstack/core';
43
+ * import { MCPServerPlugin } from '@objectstack/plugin-mcp-server';
44
+ *
45
+ * const kernel = new LiteKernel();
46
+ * kernel.use(new MCPServerPlugin({ autoStart: true }));
47
+ * await kernel.bootstrap();
48
+ * ```
49
+ */
50
+ export class MCPServerPlugin implements Plugin {
51
+ name = 'com.objectstack.plugin-mcp-server';
52
+ version = '1.0.0';
53
+ type = 'standard' as const;
54
+ dependencies: string[] = [];
55
+
56
+ private runtime?: MCPServerRuntime;
57
+ private readonly options: MCPServerPluginOptions;
58
+
59
+ constructor(options: MCPServerPluginOptions = {}) {
60
+ this.options = options;
61
+ }
62
+
63
+ async init(ctx: PluginContext): Promise<void> {
64
+ const config: MCPServerRuntimeConfig = {
65
+ name: process.env.MCP_SERVER_NAME ?? this.options.name ?? 'objectstack',
66
+ version: this.options.version ?? '1.0.0',
67
+ transport: (process.env.MCP_SERVER_TRANSPORT as 'stdio' | 'http') ?? this.options.transport ?? 'stdio',
68
+ instructions: this.options.instructions,
69
+ logger: ctx.logger,
70
+ };
71
+
72
+ this.runtime = new MCPServerRuntime(config);
73
+ ctx.registerService('mcp', this.runtime);
74
+
75
+ ctx.logger.info('[MCP] Plugin initialized');
76
+ }
77
+
78
+ async start(ctx: PluginContext): Promise<void> {
79
+ if (!this.runtime) return;
80
+
81
+ // ── Bridge tools from AIService ──
82
+ // The IAIService contract does not formally include `toolRegistry` because
83
+ // it is an implementation detail of AIService. We use duck-typing here to
84
+ // avoid a hard dependency on @objectstack/service-ai while still bridging
85
+ // tools when the full AIService implementation is present.
86
+ try {
87
+ const aiService = ctx.getService<IAIService & { toolRegistry?: ToolRegistry }>('ai');
88
+ if (aiService?.toolRegistry) {
89
+ this.runtime.bridgeTools(aiService.toolRegistry);
90
+ } else {
91
+ ctx.logger.debug('[MCP] AI service does not expose a toolRegistry, skipping tool bridging');
92
+ }
93
+ } catch {
94
+ ctx.logger.debug('[MCP] AI service not available, skipping tool bridging');
95
+ }
96
+
97
+ // ── Bridge resources from MetadataService & DataEngine ──
98
+ let metadataService: IMetadataService | undefined;
99
+ let dataEngine: IDataEngine | undefined;
100
+
101
+ try {
102
+ metadataService = ctx.getService<IMetadataService>('metadata');
103
+ } catch {
104
+ ctx.logger.debug('[MCP] Metadata service not available, skipping resource bridging');
105
+ }
106
+
107
+ try {
108
+ dataEngine = ctx.getService<IDataEngine>('data');
109
+ } catch {
110
+ ctx.logger.debug('[MCP] Data engine not available, skipping record resources');
111
+ }
112
+
113
+ if (metadataService) {
114
+ this.runtime.bridgeResources(metadataService, dataEngine);
115
+ this.runtime.bridgePrompts(metadataService);
116
+ }
117
+
118
+ // ── Auto-start if configured ──
119
+ const shouldStart = this.options.autoStart || process.env.MCP_SERVER_ENABLED === 'true';
120
+ if (shouldStart) {
121
+ await this.runtime.start();
122
+ ctx.logger.info('[MCP] Server started automatically');
123
+ } else {
124
+ ctx.logger.info(
125
+ '[MCP] Server ready but not started. Set MCP_SERVER_ENABLED=true or use autoStart option.',
126
+ );
127
+ }
128
+
129
+ // Trigger hook for other plugins to extend MCP
130
+ await ctx.trigger('mcp:ready', this.runtime);
131
+ }
132
+
133
+ async destroy(): Promise<void> {
134
+ if (this.runtime?.isStarted) {
135
+ await this.runtime.stop();
136
+ }
137
+ this.runtime = undefined;
138
+ }
139
+ }
package/src/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { AIToolDefinition, ToolCallPart, ToolResultPart } from '@objectstack/spec/contracts';
4
+
5
+ /**
6
+ * Minimal ToolRegistry interface consumed by the MCP bridge.
7
+ *
8
+ * Matches the public API of `ToolRegistry` from `@objectstack/service-ai`
9
+ * without creating a hard dependency on that package.
10
+ */
11
+ export interface ToolRegistry {
12
+ /** Return all registered tool definitions. */
13
+ getAll(): AIToolDefinition[];
14
+
15
+ /** Execute a tool call and return the result. */
16
+ execute(toolCall: ToolCallPart): Promise<ToolExecutionResult>;
17
+ }
18
+
19
+ /**
20
+ * Extended ToolResultPart with isError flag, matching service-ai's ToolExecutionResult.
21
+ */
22
+ export interface ToolExecutionResult extends ToolResultPart {
23
+ isError?: boolean;
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": [
7
+ "node"
8
+ ]
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "dist",
15
+ "node_modules",
16
+ "**/*.test.ts"
17
+ ]
18
+ }