@objectstack/mcp 8.0.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/index.cjs ADDED
@@ -0,0 +1,817 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MCPServerPlugin: () => MCPServerPlugin,
24
+ MCPServerRuntime: () => MCPServerRuntime,
25
+ OBJECTSTACK_SKILL_DESCRIPTION: () => OBJECTSTACK_SKILL_DESCRIPTION,
26
+ OBJECTSTACK_SKILL_NAME: () => OBJECTSTACK_SKILL_NAME,
27
+ registerObjectTools: () => registerObjectTools,
28
+ renderSkillMarkdown: () => renderSkillMarkdown
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/plugin.ts
33
+ var import_types = require("@objectstack/types");
34
+
35
+ // src/mcp-server-runtime.ts
36
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
37
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
38
+ var import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
39
+
40
+ // src/mcp-http-tools.ts
41
+ var import_zod = require("zod");
42
+ var DEFAULT_MAX_LIMIT = 50;
43
+ function isSystemObject(name) {
44
+ return /^sys_/i.test(name);
45
+ }
46
+ function textResult(value) {
47
+ return { content: [{ type: "text", text: jsonText(value) }] };
48
+ }
49
+ function errorResult(message) {
50
+ return { content: [{ type: "text", text: message }], isError: true };
51
+ }
52
+ function jsonText(value) {
53
+ try {
54
+ return JSON.stringify(value, null, 2);
55
+ } catch {
56
+ return String(value);
57
+ }
58
+ }
59
+ function registerObjectTools(server, bridge, options = {}) {
60
+ const allowSystem = options.allowSystemObjects === true;
61
+ const maxLimit = options.maxQueryLimit ?? DEFAULT_MAX_LIMIT;
62
+ const guard = (objectName) => {
63
+ if (!objectName || typeof objectName !== "string") return "objectName is required";
64
+ if (!allowSystem && isSystemObject(objectName)) {
65
+ return `Object "${objectName}" is a system object and is not exposed via MCP`;
66
+ }
67
+ return void 0;
68
+ };
69
+ server.registerTool(
70
+ "list_objects",
71
+ {
72
+ description: "List the data objects (tables) available in this app. Returns each object's name, label and field count.",
73
+ inputSchema: {},
74
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
75
+ },
76
+ async () => {
77
+ try {
78
+ const objects = await bridge.listObjects();
79
+ const visible = allowSystem ? objects : objects.filter((o) => !isSystemObject(o.name));
80
+ return textResult({ objects: visible, totalCount: visible.length });
81
+ } catch (err) {
82
+ return errorResult(messageOf(err));
83
+ }
84
+ }
85
+ );
86
+ server.registerTool(
87
+ "describe_object",
88
+ {
89
+ description: "Get the schema of a data object: its fields (name, type, label, required) and enabled features.",
90
+ inputSchema: { objectName: import_zod.z.string().describe('The object/table name, e.g. "task"') },
91
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
92
+ },
93
+ async ({ objectName }) => {
94
+ const bad = guard(objectName);
95
+ if (bad) return errorResult(bad);
96
+ try {
97
+ const def = await bridge.describeObject(objectName);
98
+ if (!def) return errorResult(`Object "${objectName}" not found`);
99
+ return textResult(def);
100
+ } catch (err) {
101
+ return errorResult(messageOf(err));
102
+ }
103
+ }
104
+ );
105
+ server.registerTool(
106
+ "query_records",
107
+ {
108
+ description: "Query records from an object with optional filter, field selection, sorting and pagination. Runs under the caller's permissions and row-level security.",
109
+ inputSchema: {
110
+ objectName: import_zod.z.string().describe("The object/table name"),
111
+ where: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional().describe('Filter conditions, e.g. {"status":"open"}'),
112
+ fields: import_zod.z.array(import_zod.z.string()).optional().describe("Field names to return (defaults to all)"),
113
+ limit: import_zod.z.number().int().positive().max(maxLimit).optional().describe(`Max rows (\u2264 ${maxLimit})`),
114
+ offset: import_zod.z.number().int().nonnegative().optional().describe("Rows to skip"),
115
+ orderBy: import_zod.z.array(import_zod.z.object({ field: import_zod.z.string(), order: import_zod.z.enum(["asc", "desc"]) })).optional().describe("Sort order")
116
+ },
117
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
118
+ },
119
+ async ({ objectName, where, fields, limit, offset, orderBy }) => {
120
+ const bad = guard(objectName);
121
+ if (bad) return errorResult(bad);
122
+ try {
123
+ const result = await bridge.query(objectName, {
124
+ where,
125
+ fields,
126
+ limit: Math.min(limit ?? maxLimit, maxLimit),
127
+ offset,
128
+ orderBy
129
+ });
130
+ return textResult(result);
131
+ } catch (err) {
132
+ return errorResult(messageOf(err));
133
+ }
134
+ }
135
+ );
136
+ server.registerTool(
137
+ "get_record",
138
+ {
139
+ description: "Fetch a single record by id.",
140
+ inputSchema: {
141
+ objectName: import_zod.z.string().describe("The object/table name"),
142
+ recordId: import_zod.z.string().describe("The record id")
143
+ },
144
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
145
+ },
146
+ async ({ objectName, recordId }) => {
147
+ const bad = guard(objectName);
148
+ if (bad) return errorResult(bad);
149
+ try {
150
+ const record = await bridge.get(objectName, recordId);
151
+ if (record == null) return errorResult(`Record "${recordId}" not found in "${objectName}"`);
152
+ return textResult(record);
153
+ } catch (err) {
154
+ return errorResult(messageOf(err));
155
+ }
156
+ }
157
+ );
158
+ server.registerTool(
159
+ "create_record",
160
+ {
161
+ description: "Create a new record. Runs under the caller's permissions and validations.",
162
+ inputSchema: {
163
+ objectName: import_zod.z.string().describe("The object/table name"),
164
+ data: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).describe("Field values for the new record")
165
+ },
166
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
167
+ },
168
+ async ({ objectName, data }) => {
169
+ const bad = guard(objectName);
170
+ if (bad) return errorResult(bad);
171
+ try {
172
+ return textResult(await bridge.create(objectName, data));
173
+ } catch (err) {
174
+ return errorResult(messageOf(err));
175
+ }
176
+ }
177
+ );
178
+ server.registerTool(
179
+ "update_record",
180
+ {
181
+ description: "Update fields on an existing record by id.",
182
+ inputSchema: {
183
+ objectName: import_zod.z.string().describe("The object/table name"),
184
+ recordId: import_zod.z.string().describe("The record id"),
185
+ data: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).describe("Field values to change")
186
+ },
187
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
188
+ },
189
+ async ({ objectName, recordId, data }) => {
190
+ const bad = guard(objectName);
191
+ if (bad) return errorResult(bad);
192
+ try {
193
+ return textResult(await bridge.update(objectName, recordId, data));
194
+ } catch (err) {
195
+ return errorResult(messageOf(err));
196
+ }
197
+ }
198
+ );
199
+ server.registerTool(
200
+ "delete_record",
201
+ {
202
+ description: "Delete a record by id. This is destructive.",
203
+ inputSchema: {
204
+ objectName: import_zod.z.string().describe("The object/table name"),
205
+ recordId: import_zod.z.string().describe("The record id")
206
+ },
207
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
208
+ },
209
+ async ({ objectName, recordId }) => {
210
+ const bad = guard(objectName);
211
+ if (bad) return errorResult(bad);
212
+ try {
213
+ return textResult(await bridge.remove(objectName, recordId));
214
+ } catch (err) {
215
+ return errorResult(messageOf(err));
216
+ }
217
+ }
218
+ );
219
+ }
220
+ function messageOf(err) {
221
+ if (err instanceof Error) return err.message;
222
+ if (err && typeof err === "object" && "message" in err) return String(err.message);
223
+ return String(err);
224
+ }
225
+
226
+ // src/mcp-server-runtime.ts
227
+ var import_zod2 = require("zod");
228
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
229
+ "list_objects",
230
+ "describe_object",
231
+ "query_records",
232
+ "get_record",
233
+ "aggregate_data"
234
+ ]);
235
+ var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
236
+ "delete_field"
237
+ ]);
238
+ var MCPServerRuntime = class _MCPServerRuntime {
239
+ constructor(config = {}) {
240
+ this.started = false;
241
+ this.config = {
242
+ name: "objectstack",
243
+ version: "1.0.0",
244
+ transport: "stdio",
245
+ ...config
246
+ };
247
+ this.mcpServer = new import_mcp.McpServer(
248
+ {
249
+ name: this.config.name,
250
+ version: this.config.version
251
+ },
252
+ {
253
+ capabilities: {
254
+ resources: {},
255
+ tools: {},
256
+ prompts: {},
257
+ logging: {}
258
+ },
259
+ instructions: this.config.instructions ?? "ObjectStack MCP Server \u2014 access data objects, AI tools, and agent prompts."
260
+ }
261
+ );
262
+ }
263
+ /** The underlying McpServer instance (for advanced use cases). */
264
+ get server() {
265
+ return this.mcpServer;
266
+ }
267
+ /** Whether the server is currently connected and running. */
268
+ get isStarted() {
269
+ return this.started;
270
+ }
271
+ // ── Helpers ─────────────────────────────────────────────────────
272
+ /**
273
+ * Extract the text value from a ToolExecutionResult's output.
274
+ *
275
+ * The output may be a `{ type: 'text', value: string }` object (from the
276
+ * Vercel AI SDK ToolResultPart) or any serialisable value.
277
+ */
278
+ static formatToolOutput(result) {
279
+ const output = result.output;
280
+ if (output && typeof output === "object" && "value" in output) {
281
+ return String(output.value);
282
+ }
283
+ return JSON.stringify(output ?? "");
284
+ }
285
+ // ── Tool Bridge ────────────────────────────────────────────────
286
+ /**
287
+ * Bridge all tools from the ToolRegistry to MCP tools.
288
+ *
289
+ * Each registered tool becomes an MCP tool with the same name, description,
290
+ * and JSON Schema parameters. The handler delegates to the ToolRegistry's
291
+ * execute path.
292
+ */
293
+ bridgeTools(toolRegistry) {
294
+ const tools = toolRegistry.getAll();
295
+ const logger = this.config.logger;
296
+ for (const tool of tools) {
297
+ this.registerToolFromDefinition(tool, toolRegistry);
298
+ }
299
+ logger?.info(`[MCP] Bridged ${tools.length} tools from ToolRegistry`);
300
+ }
301
+ /**
302
+ * Register a single tool on the MCP server from an AIToolDefinition.
303
+ */
304
+ registerToolFromDefinition(tool, toolRegistry) {
305
+ const logger = this.config.logger;
306
+ this.mcpServer.registerTool(
307
+ tool.name,
308
+ {
309
+ description: tool.description,
310
+ annotations: {
311
+ // Mark tools with write side-effects for destructive operations
312
+ destructiveHint: this.isDestructiveTool(tool.name),
313
+ readOnlyHint: this.isReadOnlyTool(tool.name),
314
+ openWorldHint: false
315
+ }
316
+ },
317
+ async (extra) => {
318
+ const rawExtra = extra;
319
+ const args = rawExtra.arguments ?? {};
320
+ try {
321
+ const result = await toolRegistry.execute({
322
+ type: "tool-call",
323
+ toolCallId: `mcp-${tool.name}-${Date.now()}`,
324
+ toolName: tool.name,
325
+ input: args
326
+ });
327
+ const outputText = _MCPServerRuntime.formatToolOutput(result);
328
+ if (result.isError) {
329
+ return {
330
+ content: [{ type: "text", text: outputText }],
331
+ isError: true
332
+ };
333
+ }
334
+ return {
335
+ content: [{ type: "text", text: outputText }]
336
+ };
337
+ } catch (err) {
338
+ const message = err instanceof Error ? err.message : String(err);
339
+ logger?.warn(`[MCP] Tool "${tool.name}" execution failed:`, { error: message });
340
+ return {
341
+ content: [{ type: "text", text: message }],
342
+ isError: true
343
+ };
344
+ }
345
+ }
346
+ );
347
+ }
348
+ /**
349
+ * Check if a tool is read-only (data query tools).
350
+ */
351
+ isReadOnlyTool(name) {
352
+ return READ_ONLY_TOOLS.has(name);
353
+ }
354
+ /**
355
+ * Check if a tool performs destructive operations.
356
+ */
357
+ isDestructiveTool(name) {
358
+ return DESTRUCTIVE_TOOLS.has(name);
359
+ }
360
+ // ── Resource Bridge ────────────────────────────────────────────
361
+ /**
362
+ * Bridge metadata service and data engine to MCP resources.
363
+ *
364
+ * Exposes:
365
+ * - `objectstack://objects` — List all data objects
366
+ * - `objectstack://objects/{objectName}` — Get object schema
367
+ * - `objectstack://objects/{objectName}/records/{recordId}` — Get a specific record
368
+ * - `objectstack://metadata/types` — List all metadata types
369
+ */
370
+ bridgeResources(metadataService, dataEngine) {
371
+ const logger = this.config.logger;
372
+ let resourceCount = 0;
373
+ this.mcpServer.registerResource(
374
+ "object_list",
375
+ "objectstack://objects",
376
+ {
377
+ description: "List all data objects (tables) in the ObjectStack instance",
378
+ mimeType: "application/json"
379
+ },
380
+ async () => {
381
+ const objects = await metadataService.listObjects();
382
+ const summary = objects.map((o) => ({
383
+ name: o.name,
384
+ label: o.label ?? o.name,
385
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0
386
+ }));
387
+ return {
388
+ contents: [{
389
+ uri: "objectstack://objects",
390
+ mimeType: "application/json",
391
+ text: JSON.stringify({ objects: summary, totalCount: summary.length }, null, 2)
392
+ }]
393
+ };
394
+ }
395
+ );
396
+ resourceCount++;
397
+ this.mcpServer.registerResource(
398
+ "object_schema",
399
+ new import_mcp.ResourceTemplate("objectstack://objects/{objectName}", { list: void 0 }),
400
+ {
401
+ description: "Get the full schema of a specific data object including fields and features",
402
+ mimeType: "application/json"
403
+ },
404
+ async (_uri, variables) => {
405
+ const objectName = String(variables.objectName);
406
+ const objectDef = await metadataService.getObject(objectName);
407
+ if (!objectDef) {
408
+ return {
409
+ contents: [{
410
+ uri: `objectstack://objects/${objectName}`,
411
+ mimeType: "application/json",
412
+ text: JSON.stringify({ error: `Object "${objectName}" not found` })
413
+ }]
414
+ };
415
+ }
416
+ const def = objectDef;
417
+ const fields = def.fields ?? {};
418
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
419
+ name: key,
420
+ type: f.type,
421
+ label: f.label ?? key,
422
+ required: f.required ?? false
423
+ }));
424
+ return {
425
+ contents: [{
426
+ uri: `objectstack://objects/${objectName}`,
427
+ mimeType: "application/json",
428
+ text: JSON.stringify({
429
+ name: def.name,
430
+ label: def.label ?? def.name,
431
+ fields: fieldSummary,
432
+ enableFeatures: def.enable ?? {}
433
+ }, null, 2)
434
+ }]
435
+ };
436
+ }
437
+ );
438
+ resourceCount++;
439
+ if (dataEngine) {
440
+ this.mcpServer.registerResource(
441
+ "record_by_id",
442
+ new import_mcp.ResourceTemplate("objectstack://objects/{objectName}/records/{recordId}", { list: void 0 }),
443
+ {
444
+ description: "Get a specific record by ID from a data object",
445
+ mimeType: "application/json"
446
+ },
447
+ async (_uri, variables) => {
448
+ const objectName = String(variables.objectName);
449
+ const recordId = String(variables.recordId);
450
+ try {
451
+ const record = await dataEngine.findOne(objectName, {
452
+ where: { id: recordId }
453
+ });
454
+ if (!record) {
455
+ return {
456
+ contents: [{
457
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
458
+ mimeType: "application/json",
459
+ text: JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` })
460
+ }]
461
+ };
462
+ }
463
+ return {
464
+ contents: [{
465
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
466
+ mimeType: "application/json",
467
+ text: JSON.stringify(record, null, 2)
468
+ }]
469
+ };
470
+ } catch (err) {
471
+ const message = err instanceof Error ? err.message : String(err);
472
+ return {
473
+ contents: [{
474
+ uri: `objectstack://objects/${objectName}/records/${recordId}`,
475
+ mimeType: "application/json",
476
+ text: JSON.stringify({ error: message })
477
+ }]
478
+ };
479
+ }
480
+ }
481
+ );
482
+ resourceCount++;
483
+ }
484
+ if (metadataService.getRegisteredTypes) {
485
+ this.mcpServer.registerResource(
486
+ "metadata_types",
487
+ "objectstack://metadata/types",
488
+ {
489
+ description: "List all registered metadata types (object, app, view, agent, tool, etc.)",
490
+ mimeType: "application/json"
491
+ },
492
+ async () => {
493
+ const types = await metadataService.getRegisteredTypes();
494
+ return {
495
+ contents: [{
496
+ uri: "objectstack://metadata/types",
497
+ mimeType: "application/json",
498
+ text: JSON.stringify({ types, totalCount: types.length }, null, 2)
499
+ }]
500
+ };
501
+ }
502
+ );
503
+ resourceCount++;
504
+ }
505
+ logger?.info(`[MCP] Bridged ${resourceCount} resource endpoints`);
506
+ }
507
+ // ── Prompt Bridge ──────────────────────────────────────────────
508
+ /**
509
+ * Bridge registered agents to MCP prompts.
510
+ *
511
+ * Each active agent becomes an MCP prompt with:
512
+ * - Name matching the agent name
513
+ * - System message from agent instructions
514
+ * - Optional context arguments (objectName, recordId, viewName)
515
+ */
516
+ bridgePrompts(metadataService) {
517
+ const logger = this.config.logger;
518
+ this.mcpServer.registerPrompt(
519
+ "agent_prompt",
520
+ {
521
+ description: "Load an agent's system prompt with optional UI context. Use the agentName argument to select which agent's instructions to use.",
522
+ argsSchema: {
523
+ agentName: import_zod2.z.string().describe('Name of the agent to load (e.g. "data_chat", "metadata_assistant")'),
524
+ objectName: import_zod2.z.string().optional().describe("Current object the user is viewing"),
525
+ recordId: import_zod2.z.string().optional().describe("Currently selected record ID"),
526
+ viewName: import_zod2.z.string().optional().describe("Current view name")
527
+ }
528
+ },
529
+ async (args) => {
530
+ const agentName = String(args.agentName ?? "");
531
+ if (!agentName) {
532
+ return {
533
+ messages: [{
534
+ role: "user",
535
+ content: { type: "text", text: "Error: agentName argument is required" }
536
+ }]
537
+ };
538
+ }
539
+ const raw = await metadataService.get("agent", agentName);
540
+ if (!raw) {
541
+ return {
542
+ messages: [{
543
+ role: "user",
544
+ content: { type: "text", text: `Error: Agent "${agentName}" not found` }
545
+ }]
546
+ };
547
+ }
548
+ const agent = raw;
549
+ const parts = [];
550
+ parts.push(agent.instructions ?? "");
551
+ const contextHints = [];
552
+ if (args.objectName) contextHints.push(`Current object: ${args.objectName}`);
553
+ if (args.recordId) contextHints.push(`Selected record ID: ${args.recordId}`);
554
+ if (args.viewName) contextHints.push(`Current view: ${args.viewName}`);
555
+ if (contextHints.length > 0) {
556
+ parts.push("\n--- Current Context ---\n" + contextHints.join("\n"));
557
+ }
558
+ return {
559
+ messages: [{
560
+ role: "assistant",
561
+ content: { type: "text", text: parts.join("\n") }
562
+ }]
563
+ };
564
+ }
565
+ );
566
+ logger?.info("[MCP] Agent prompts bridged");
567
+ }
568
+ // ── Lifecycle ──────────────────────────────────────────────────
569
+ /**
570
+ * Start the MCP server with the configured transport.
571
+ *
572
+ * For stdio transport, this connects to process stdin/stdout.
573
+ */
574
+ async start() {
575
+ if (this.started) return;
576
+ const logger = this.config.logger;
577
+ if (this.config.transport === "stdio") {
578
+ this.transport = new import_stdio.StdioServerTransport();
579
+ await this.mcpServer.connect(this.transport);
580
+ this.started = true;
581
+ logger?.info(`[MCP] Server started (transport: stdio, name: ${this.config.name})`);
582
+ } else {
583
+ logger?.info("[MCP] HTTP transport ready (served per-request at /api/v1/mcp).");
584
+ }
585
+ }
586
+ /**
587
+ * Stop the MCP server and disconnect the transport.
588
+ */
589
+ async stop() {
590
+ if (!this.started) return;
591
+ await this.mcpServer.close();
592
+ this.transport = void 0;
593
+ this.started = false;
594
+ this.config.logger?.info("[MCP] Server stopped");
595
+ }
596
+ // ── HTTP (Streamable HTTP) transport ───────────────────────────
597
+ /**
598
+ * Handle one MCP request over the **Streamable HTTP** transport (Web Standard
599
+ * `Request`/`Response`), the network-reachable surface for external agents.
600
+ *
601
+ * Stateless by design: a fresh {@link McpServer} + transport is built per
602
+ * request (the SDK-recommended pattern for stateless HTTP — it avoids any
603
+ * cross-request session/request-id collision and keeps each call isolated).
604
+ * The tool set is the object-CRUD bridge, bound to the **caller's principal**
605
+ * via `bridge`; the runtime wires that bridge to the existing permission +
606
+ * RLS path, so an external agent can never exceed the key's authority.
607
+ *
608
+ * Only the object-CRUD tools are exposed here — the internal AI/authoring
609
+ * toolRegistry (which can mutate metadata) is deliberately NOT bridged onto
610
+ * the external surface.
611
+ *
612
+ * @param request The inbound Web `Request` (headers/method/url).
613
+ * @param opts.bridge Principal-bound data accessor (required to expose tools).
614
+ * @param opts.parsedBody Pre-parsed JSON-RPC body (the dispatcher already read it).
615
+ * @param opts.authInfo Optional auth info forwarded to message handlers.
616
+ * @param opts.toolOptions Object-tool exposure options (system objects, limits).
617
+ */
618
+ async handleHttpRequest(request, opts = {}) {
619
+ const server = new import_mcp.McpServer(
620
+ { name: this.config.name, version: this.config.version },
621
+ {
622
+ capabilities: { tools: {} },
623
+ instructions: this.config.instructions ?? "ObjectStack MCP Server \u2014 query and modify your app's data objects as tools."
624
+ }
625
+ );
626
+ if (opts.bridge) {
627
+ registerObjectTools(server, opts.bridge, opts.toolOptions);
628
+ }
629
+ const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
630
+ // Stateless: no session id, single request/response.
631
+ sessionIdGenerator: void 0,
632
+ // Return a buffered JSON response (no long-lived SSE) — fits the
633
+ // Worker→container hop without streaming pass-through concerns.
634
+ enableJsonResponse: true
635
+ });
636
+ await server.connect(transport);
637
+ try {
638
+ return await transport.handleRequest(request, {
639
+ parsedBody: opts.parsedBody,
640
+ authInfo: opts.authInfo
641
+ });
642
+ } finally {
643
+ await server.close().catch(() => {
644
+ });
645
+ }
646
+ }
647
+ };
648
+
649
+ // src/plugin.ts
650
+ var MCPServerPlugin = class {
651
+ constructor(options = {}) {
652
+ this.name = "com.objectstack.mcp";
653
+ this.version = "1.0.0";
654
+ this.type = "standard";
655
+ this.dependencies = [];
656
+ this.options = options;
657
+ }
658
+ async init(ctx) {
659
+ const config = {
660
+ name: (0, import_types.readEnvWithDeprecation)("OS_MCP_SERVER_NAME", "MCP_SERVER_NAME") ?? this.options.name ?? "objectstack",
661
+ version: this.options.version ?? "1.0.0",
662
+ transport: (0, import_types.readEnvWithDeprecation)("OS_MCP_SERVER_TRANSPORT", "MCP_SERVER_TRANSPORT") ?? this.options.transport ?? "stdio",
663
+ instructions: this.options.instructions,
664
+ logger: ctx.logger
665
+ };
666
+ this.runtime = new MCPServerRuntime(config);
667
+ ctx.registerService("mcp", this.runtime);
668
+ ctx.logger.info("[MCP] Plugin initialized");
669
+ }
670
+ async start(ctx) {
671
+ if (!this.runtime) return;
672
+ try {
673
+ const aiService = ctx.getService("ai");
674
+ if (aiService?.toolRegistry) {
675
+ this.runtime.bridgeTools(aiService.toolRegistry);
676
+ } else {
677
+ ctx.logger.debug("[MCP] AI service does not expose a toolRegistry, skipping tool bridging");
678
+ }
679
+ } catch {
680
+ ctx.logger.debug("[MCP] AI service not available, skipping tool bridging");
681
+ }
682
+ let metadataService;
683
+ let dataEngine;
684
+ try {
685
+ metadataService = ctx.getService("metadata");
686
+ } catch {
687
+ ctx.logger.debug("[MCP] Metadata service not available, skipping resource bridging");
688
+ }
689
+ try {
690
+ dataEngine = ctx.getService("data");
691
+ } catch {
692
+ ctx.logger.debug("[MCP] Data engine not available, skipping record resources");
693
+ }
694
+ if (metadataService) {
695
+ this.runtime.bridgeResources(metadataService, dataEngine);
696
+ this.runtime.bridgePrompts(metadataService);
697
+ }
698
+ const shouldStart = this.options.autoStart || (0, import_types.readEnvWithDeprecation)("OS_MCP_SERVER_ENABLED", "MCP_SERVER_ENABLED") === "true";
699
+ if (shouldStart) {
700
+ await this.runtime.start();
701
+ ctx.logger.info("[MCP] Server started automatically");
702
+ } else {
703
+ ctx.logger.info(
704
+ "[MCP] Server ready but not started. Set OS_MCP_SERVER_ENABLED=true or use autoStart option."
705
+ );
706
+ }
707
+ await ctx.trigger("mcp:ready", this.runtime);
708
+ }
709
+ async destroy() {
710
+ if (this.runtime?.isStarted) {
711
+ await this.runtime.stop();
712
+ }
713
+ this.runtime = void 0;
714
+ }
715
+ };
716
+
717
+ // src/skill.ts
718
+ var OBJECTSTACK_SKILL_NAME = "objectstack";
719
+ var OBJECTSTACK_SKILL_DESCRIPTION = "Query and modify data in an ObjectStack app over MCP \u2014 discover objects, read and filter records, and create/update/delete under your own permissions and row-level security. Use when the user wants to inspect or change data in their ObjectStack environment.";
720
+ var URL_PLACEHOLDER = "<YOUR_ENV_MCP_URL>";
721
+ function renderSkillMarkdown(options = {}) {
722
+ const url = options.mcpUrl?.trim() || URL_PLACEHOLDER;
723
+ const envLabel = options.envName?.trim();
724
+ const intro = envLabel ? `This skill connects you to the **${envLabel}** ObjectStack environment.` : "This skill connects you to an ObjectStack environment.";
725
+ return `---
726
+ name: ${OBJECTSTACK_SKILL_NAME}
727
+ description: ${OBJECTSTACK_SKILL_DESCRIPTION}
728
+ ---
729
+
730
+ # ObjectStack
731
+
732
+ ${intro} An ObjectStack environment exposes its data **objects** (tables) as
733
+ tools over the Model Context Protocol (MCP). Every operation runs **as you** \u2014
734
+ under your account's permissions and row-level security \u2014 so you may see a
735
+ subset of rows, or get a permission error on a write. That is expected
736
+ governance, not a failure.
737
+
738
+ ## When to use
739
+
740
+ Use these tools whenever the user wants to **inspect or change data** in their
741
+ ObjectStack app: look up records, filter/report, create or update entries, or
742
+ clean up data. Prefer these tools over guessing \u2014 the environment is the source
743
+ of truth.
744
+
745
+ ## Connect
746
+
747
+ This skill drives the MCP server at:
748
+
749
+ \`\`\`
750
+ ${url}
751
+ \`\`\`
752
+
753
+ Authenticate with an ObjectStack API key sent as a request header (the key is
754
+ shown to you once when created; treat it like a password):
755
+
756
+ \`\`\`
757
+ x-api-key: <YOUR_API_KEY>
758
+ \`\`\`
759
+
760
+ (The header \`Authorization: ApiKey <YOUR_API_KEY>\` is also accepted.) If your
761
+ MCP client supports custom headers on a remote server, set the header there.
762
+
763
+ ## Discover before you act
764
+
765
+ The schema is **not** baked into this skill \u2014 it is discovered live, so it is
766
+ always current even as the app evolves:
767
+
768
+ 1. \`list_objects\` \u2014 see what objects exist.
769
+ 2. \`describe_object({ objectName })\` \u2014 get an object's fields (name, type,
770
+ required) before querying or writing it.
771
+
772
+ Always discover the relevant object's shape before constructing a filter or a
773
+ create/update payload.
774
+
775
+ ## Tools
776
+
777
+ - **list_objects()** \u2014 list available objects (system \`sys_*\` objects are hidden).
778
+ - **describe_object({ objectName })** \u2014 an object's fields and features.
779
+ - **query_records({ objectName, where?, fields?, limit?, offset?, orderBy? })** \u2014
780
+ read records. \`where\` is a field\u2192value match, e.g. \`{ "status": "open" }\`.
781
+ Results are page-capped; use \`limit\`/\`offset\` to page.
782
+ - **get_record({ objectName, recordId })** \u2014 fetch one record by id.
783
+ - **create_record({ objectName, data })** \u2014 create a record.
784
+ - **update_record({ objectName, recordId, data })** \u2014 change fields on a record.
785
+ - **delete_record({ objectName, recordId })** \u2014 delete a record (destructive \u2014
786
+ confirm with the user first).
787
+
788
+ ## Conventions & gotchas
789
+
790
+ - **Permissions/RLS apply to every call.** Fewer rows than expected, or a
791
+ write that's rejected, usually means your key isn't authorized \u2014 don't retry
792
+ blindly; tell the user.
793
+ - **Discover, don't assume.** Object and field names vary per app; always
794
+ \`list_objects\` / \`describe_object\` first.
795
+ - **Writes are real and immediate.** There is no implicit dry-run. Confirm
796
+ destructive actions (\`delete_record\`, bulk updates) with the user.
797
+ - **Page large reads.** Use \`limit\`/\`offset\` rather than asking for everything.
798
+
799
+ ## Recommended workflow
800
+
801
+ 1. \`list_objects\` to orient.
802
+ 2. \`describe_object\` on the target object.
803
+ 3. \`query_records\` to read / verify current state.
804
+ 4. \`create_record\` / \`update_record\` / \`delete_record\` to make changes,
805
+ confirming destructive steps with the user.
806
+ `;
807
+ }
808
+ // Annotate the CommonJS export names for ESM import in node:
809
+ 0 && (module.exports = {
810
+ MCPServerPlugin,
811
+ MCPServerRuntime,
812
+ OBJECTSTACK_SKILL_DESCRIPTION,
813
+ OBJECTSTACK_SKILL_NAME,
814
+ registerObjectTools,
815
+ renderSkillMarkdown
816
+ });
817
+ //# sourceMappingURL=index.cjs.map