@dbx-tools/appkit-mastra 0.1.57 → 0.1.67

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.
Files changed (44) hide show
  1. package/README.md +41 -1
  2. package/dist/index.d.ts +1229 -17
  3. package/dist/index.js +3348 -19
  4. package/package.json +17 -27
  5. package/src/agents.ts +7 -1
  6. package/src/config.ts +64 -0
  7. package/src/genie.ts +9 -21
  8. package/{index.ts → src/index.ts} +7 -9
  9. package/src/mcp.ts +89 -0
  10. package/src/model.ts +9 -3
  11. package/src/plugin.ts +51 -8
  12. package/src/serving.ts +5 -9
  13. package/src/statement.ts +8 -36
  14. package/dist/src/agents.d.ts +0 -316
  15. package/dist/src/agents.js +0 -470
  16. package/dist/src/chart.d.ts +0 -153
  17. package/dist/src/chart.js +0 -570
  18. package/dist/src/config.d.ts +0 -295
  19. package/dist/src/config.js +0 -55
  20. package/dist/src/genie.d.ts +0 -161
  21. package/dist/src/genie.js +0 -973
  22. package/dist/src/history.d.ts +0 -95
  23. package/dist/src/history.js +0 -278
  24. package/dist/src/memory.d.ts +0 -109
  25. package/dist/src/memory.js +0 -253
  26. package/dist/src/model.d.ts +0 -52
  27. package/dist/src/model.js +0 -198
  28. package/dist/src/observability.d.ts +0 -64
  29. package/dist/src/observability.js +0 -83
  30. package/dist/src/plugin.d.ts +0 -177
  31. package/dist/src/plugin.js +0 -554
  32. package/dist/src/processor.d.ts +0 -8
  33. package/dist/src/processor.js +0 -40
  34. package/dist/src/processors/strip-stale-charts.d.ts +0 -32
  35. package/dist/src/processors/strip-stale-charts.js +0 -98
  36. package/dist/src/server.d.ts +0 -51
  37. package/dist/src/server.js +0 -152
  38. package/dist/src/serving.d.ts +0 -65
  39. package/dist/src/serving.js +0 -79
  40. package/dist/src/statement.d.ts +0 -66
  41. package/dist/src/statement.js +0 -111
  42. package/dist/src/writer.d.ts +0 -23
  43. package/dist/src/writer.js +0 -37
  44. package/dist/tsconfig.build.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,26 +1,18 @@
1
1
  {
2
- "main": "./dist/index.js",
3
- "types": "./dist/index.d.ts",
4
- "exports": {
5
- ".": {
6
- "source": "./index.ts",
7
- "types": "./dist/index.d.ts",
8
- "default": "./dist/index.js"
9
- }
10
- },
11
2
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.57",
3
+ "version": "0.1.67",
13
4
  "dependencies": {
14
5
  "@databricks/sdk-experimental": "^0.17",
15
- "@dbx-tools/appkit-mastra-shared": "0.1.57",
16
- "@dbx-tools/genie": "0.1.57",
17
- "@dbx-tools/genie-shared": "0.1.57",
18
- "@dbx-tools/model": "0.1.57",
19
- "@dbx-tools/shared": "0.1.57",
6
+ "@dbx-tools/appkit-mastra-shared": "0.1.67",
7
+ "@dbx-tools/genie": "0.1.67",
8
+ "@dbx-tools/genie-shared": "0.1.67",
9
+ "@dbx-tools/model": "0.1.67",
10
+ "@dbx-tools/shared": "0.1.67",
20
11
  "@mastra/ai-sdk": "^1",
21
12
  "@mastra/core": "^1",
22
13
  "@mastra/express": "^1",
23
14
  "@mastra/fastembed": "^1",
15
+ "@mastra/mcp": "^1",
24
16
  "@mastra/memory": "^1",
25
17
  "@mastra/observability": "^1",
26
18
  "@mastra/otel-bridge": "^1",
@@ -28,25 +20,23 @@
28
20
  "pg": "^8.20.0",
29
21
  "zod": "^4.3.6"
30
22
  },
31
- "module": "index.ts",
23
+ "exports": {
24
+ ".": {
25
+ "source": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
32
30
  "peerDependencies": {
33
31
  "@databricks/appkit": "^0.41",
34
32
  "express": "^5"
35
33
  },
36
34
  "type": "module",
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
37
  "files": [
38
38
  "dist",
39
- "index*.ts",
40
39
  "src"
41
40
  ],
42
- "license": "Apache-2.0",
43
- "homepage": "https://github.com/reggie-db/dbx-tools-js#readme",
44
- "bugs": {
45
- "url": "https://github.com/reggie-db/dbx-tools-js/issues"
46
- },
47
- "repository": {
48
- "type": "git",
49
- "url": "git+https://github.com/reggie-db/dbx-tools-js.git",
50
- "directory": "packages/appkit-mastra"
51
- }
41
+ "license": "Apache-2.0"
52
42
  }
package/src/agents.ts CHANGED
@@ -319,6 +319,12 @@ export type MastraMemoryConfigOverride = DistributiveOmit<PgVectorConfig, "id">
319
319
  export interface BuiltAgents {
320
320
  agents: Record<string, Agent>;
321
321
  defaultAgentId: string;
322
+ /**
323
+ * Ambient tools shared across every agent (the built-in system tools
324
+ * spread with `config.tools`). Surfaced so the optional MCP server
325
+ * can re-expose them when {@link MastraMcpConfig.tools} is enabled.
326
+ */
327
+ ambientTools: MastraTools;
322
328
  }
323
329
 
324
330
  /** Fallback agent id used when `config.agents` is omitted entirely. */
@@ -477,7 +483,7 @@ export async function buildAgents(opts: {
477
483
  }
478
484
 
479
485
  log.info("agents ready", { ids, defaultAgentId });
480
- return { agents, defaultAgentId };
486
+ return { agents, defaultAgentId, ambientTools };
481
487
  }
482
488
 
483
489
  /**
package/src/config.ts CHANGED
@@ -80,6 +80,45 @@ export type MastraMemoryConfig = PgVectorConfig & {
80
80
  id?: string;
81
81
  };
82
82
 
83
+ /**
84
+ * Fine-grained control for the optional MCP server exposure
85
+ * ({@link MastraPluginConfig.mcp}). Every field is optional; the object
86
+ * form only needs to set what differs from the defaults.
87
+ */
88
+ export interface MastraMcpConfig {
89
+ /**
90
+ * Server id used in the route path (`/mcp/<serverId>/...`) and as the
91
+ * MCP registry id. Defaults to the plugin's registered name.
92
+ */
93
+ serverId?: string;
94
+ /** Display name advertised over MCP. Defaults to `"<displayName> MCP"`. */
95
+ name?: string;
96
+ /** Semantic version advertised over MCP. Defaults to `"1.0.0"`. */
97
+ version?: string;
98
+ /** Optional human-readable description advertised over MCP. */
99
+ description?: string;
100
+ /**
101
+ * Expose every registered agent as an `ask_<agentId>` MCP tool.
102
+ * Defaults to `true` - this is the "leverage the Mastra agents over
103
+ * MCP" behavior most callers want.
104
+ */
105
+ agents?: boolean;
106
+ /**
107
+ * Also expose the plugin's ambient tools (the built-in `render_data`
108
+ * plus anything in `config.tools`) as MCP tools. Defaults to `false`:
109
+ * the ambient tools assume an in-process chat turn (they publish
110
+ * writer events the chat UI consumes), so they aren't useful to a
111
+ * standalone MCP client. Turn this on only when those tools are safe
112
+ * to call out-of-band.
113
+ */
114
+ tools?: boolean;
115
+ /**
116
+ * Extra tools to expose over MCP beyond the agent / ambient sets.
117
+ * Use this for tools written specifically for MCP consumers.
118
+ */
119
+ extraTools?: MastraTools;
120
+ }
121
+
83
122
  /** Configuration accepted by the Mastra AppKit plugin. */
84
123
  export interface MastraPluginConfig extends BasePluginConfig {
85
124
  /** Mastra OpenAI-compatible provider id. Defaults to `"databricks"`. */
@@ -317,4 +356,29 @@ export interface MastraPluginConfig extends BasePluginConfig {
317
356
  * to drill deep into a dataset within a single turn.
318
357
  */
319
358
  agentMaxSteps?: number;
359
+ /**
360
+ * Expose the plugin's agents (and optionally tools) as a Mastra MCP
361
+ * server so external MCP clients - Claude Desktop, Cursor, the Mastra
362
+ * playground, or another agent - can call them over the standard MCP
363
+ * transports. Disabled by default.
364
+ *
365
+ * - `undefined` / `false`: no MCP endpoints.
366
+ * - `true`: expose every registered agent as an `ask_<agentId>` MCP
367
+ * tool under a server whose id is the plugin name.
368
+ * - {@link MastraMcpConfig}: fine-grained control over the server id,
369
+ * advertised metadata, and which agents / tools are exposed.
370
+ *
371
+ * When enabled, the stock Mastra MCP routes mount under the plugin's
372
+ * base path (no bespoke route is added - the server is handed to the
373
+ * `Mastra` instance via `mcpServers`, which `@mastra/express` serves):
374
+ *
375
+ * - Streamable HTTP: `POST /api/<plugin>/mcp/<serverId>/mcp`
376
+ * - SSE (legacy): `GET /api/<plugin>/mcp/<serverId>/sse`
377
+ * `POST /api/<plugin>/mcp/<serverId>/messages`
378
+ *
379
+ * Requests run under the same AppKit OBO scope as the chat routes, so
380
+ * an agent invoked over MCP resolves its model and tools as the
381
+ * calling user.
382
+ */
383
+ mcp?: boolean | MastraMcpConfig;
320
384
  }
package/src/genie.ts CHANGED
@@ -35,7 +35,7 @@
35
35
  */
36
36
 
37
37
  import { CacheManager, genie } from "@databricks/appkit";
38
- import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
38
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
39
39
  import {
40
40
  ChartSchema,
41
41
  type MastraWriter,
@@ -43,7 +43,13 @@ import {
43
43
  } from "@dbx-tools/appkit-mastra-shared";
44
44
  import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
45
45
  import { GenieMessageSchema, type GenieMessage } from "@dbx-tools/genie-shared";
46
- import { appkitUtils, commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
46
+ import {
47
+ apiUtils,
48
+ appkitUtils,
49
+ commonUtils,
50
+ logUtils,
51
+ stringUtils,
52
+ } from "@dbx-tools/shared";
47
53
  import type { RequestContext } from "@mastra/core/request-context";
48
54
  import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
49
55
  import { createTool } from "@mastra/core/tools";
@@ -302,24 +308,6 @@ async function ensureConversationSeeded(
302
308
  if (cached) writeContextConversationId(requestContext, spaceId, cached);
303
309
  }
304
310
 
305
- /**
306
- * True when `err` is the SDK error Genie returns for a
307
- * conversation id that no longer exists (deleted, expired upstream,
308
- * or referenced from the wrong space). Matches the typed
309
- * {@link ApiError} 404 / `RESOURCE_DOES_NOT_EXIST` shape first, then
310
- * falls back to the lower-level {@link HttpError} 404, then to a
311
- * loose message sniff for SDK shapes we haven't catalogued.
312
- */
313
- function isConversationGoneError(err: unknown): boolean {
314
- if (err instanceof ApiError) {
315
- if (err.statusCode === 404) return true;
316
- if (err.errorCode === "RESOURCE_DOES_NOT_EXIST") return true;
317
- }
318
- if (err instanceof HttpError && err.code === 404) return true;
319
- if (err instanceof Error && /does not exist/i.test(err.message)) return true;
320
- return false;
321
- }
322
-
323
311
  /* ------------------------ prepare_chart input ------------------------ */
324
312
 
325
313
  /**
@@ -506,7 +494,7 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
506
494
  // and try once more. Only retry when we *had* a seeded id -
507
495
  // a fresh call that 404s shouldn't loop.
508
496
  const seeded = readContextConversationId(requestContext, spaceId);
509
- if (seeded && isConversationGoneError(err)) {
497
+ if (seeded && apiUtils.isNotFoundError(err)) {
510
498
  log.warn("conversation-cache:stale, resetting", {
511
499
  spaceId,
512
500
  conversationId: seeded,
@@ -12,16 +12,14 @@
12
12
  */
13
13
  export * from "@dbx-tools/appkit-mastra-shared";
14
14
  export * from "@dbx-tools/model";
15
- export * from "./src/agents.js";
16
- export * from "./src/chart.js";
17
- export * from "./src/config.js";
18
- export * from "./src/genie.js";
19
- export * from "./src/plugin.js";
15
+ export * from "./agents.js";
16
+ export * from "./chart.js";
17
+ export * from "./config.js";
18
+ export * from "./genie.js";
19
+ export * from "./mcp.js";
20
+ export * from "./plugin.js";
20
21
  export {
21
22
  MASTRA_MODEL_OVERRIDE_KEY,
22
- MODEL_OVERRIDE_BODY_FIELDS,
23
- MODEL_OVERRIDE_HEADER,
24
- MODEL_OVERRIDE_QUERY,
25
23
  extractModelOverride,
26
24
  type ModelOverrideRequest,
27
- } from "./src/serving.js";
25
+ } from "./serving.js";
package/src/mcp.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Optional Mastra MCP server exposure for the AppKit Mastra plugin.
3
+ *
4
+ * Turns the plugin's registered agents (and, opt-in, its ambient tools)
5
+ * into a Mastra `MCPServer` so external MCP clients - Claude Desktop,
6
+ * Cursor, the Mastra playground, or another agent - can call them over
7
+ * the standard MCP transports. The resulting server is handed to the
8
+ * `Mastra` instance via `mcpServers`, which makes `@mastra/express`
9
+ * serve the stock MCP routes under the plugin's base path; the plugin
10
+ * never registers a bespoke MCP route of its own.
11
+ */
12
+
13
+ import type { Agent } from "@mastra/core/agent";
14
+ import { MCPServer } from "@mastra/mcp";
15
+
16
+ import type { MastraTools } from "./agents.js";
17
+ import type { MastraMcpConfig, MastraPluginConfig } from "./config.js";
18
+
19
+ /** MCP server version advertised when the caller doesn't pin one. */
20
+ const DEFAULT_MCP_VERSION = "1.0.0";
21
+
22
+ /**
23
+ * A built MCP server plus the request paths it answers on, relative to
24
+ * the plugin's base path (`/api/<plugin>`). The paths are informational
25
+ * (logging / `exports().getMcp()`); the routes themselves are mounted
26
+ * by `@mastra/express` off the `Mastra` instance's `mcpServers`.
27
+ */
28
+ export interface ResolvedMcp {
29
+ /** Registry id, used in the route path (`/mcp/<serverId>/...`). */
30
+ serverId: string;
31
+ /** The Mastra MCP server to hand to `new Mastra({ mcpServers })`. */
32
+ server: MCPServer;
33
+ /** Streamable-HTTP transport path, relative to the plugin base path. */
34
+ httpPath: string;
35
+ /** SSE transport path, relative to the plugin base path. */
36
+ ssePath: string;
37
+ /** SSE message path, relative to the plugin base path. */
38
+ messagePath: string;
39
+ }
40
+
41
+ /**
42
+ * Build the plugin's MCP server, or `null` when `config.mcp` is falsy
43
+ * (the default - no MCP endpoints).
44
+ *
45
+ * `true` exposes every registered agent as an `ask_<agentId>` MCP tool
46
+ * under a server id equal to the plugin name. The object form
47
+ * ({@link MastraMcpConfig}) tunes the id / advertised metadata and can
48
+ * additionally expose the plugin's ambient tools or a set of extra
49
+ * MCP-only tools.
50
+ */
51
+ export function buildMcpServer(opts: {
52
+ config: MastraPluginConfig;
53
+ pluginName: string;
54
+ displayName: string;
55
+ agents: Record<string, Agent>;
56
+ ambientTools: MastraTools;
57
+ }): ResolvedMcp | null {
58
+ const { config, pluginName, displayName, agents, ambientTools } = opts;
59
+ if (!config.mcp) return null;
60
+ const settings: MastraMcpConfig = config.mcp === true ? {} : config.mcp;
61
+
62
+ const serverId = settings.serverId ?? pluginName;
63
+ const exposeAgents = settings.agents !== false;
64
+ const exposeTools = settings.tools === true;
65
+
66
+ // MCPServerConfig.tools is required, so always pass a record (empty
67
+ // when the caller exposes only agents).
68
+ const tools: MastraTools = {
69
+ ...(exposeTools ? ambientTools : {}),
70
+ ...(settings.extraTools ?? {}),
71
+ };
72
+
73
+ const server = new MCPServer({
74
+ id: serverId,
75
+ name: settings.name ?? `${displayName} MCP`,
76
+ version: settings.version ?? DEFAULT_MCP_VERSION,
77
+ ...(settings.description ? { description: settings.description } : {}),
78
+ ...(exposeAgents ? { agents } : {}),
79
+ tools,
80
+ });
81
+
82
+ return {
83
+ serverId,
84
+ server,
85
+ httpPath: `/mcp/${serverId}/mcp`,
86
+ ssePath: `/mcp/${serverId}/sse`,
87
+ messagePath: `/mcp/${serverId}/messages`,
88
+ };
89
+ }
package/src/model.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  * propagate so callers see the real SDK message.
22
22
  */
23
23
 
24
+ import { getExecutionContext } from "@databricks/appkit";
24
25
  import { type ModelClass, parseModelClass, selectModel } from "@dbx-tools/model";
25
26
  import { commonUtils, logUtils, netUtils, stringUtils } from "@dbx-tools/shared";
26
27
  import type { MastraModelConfig } from "@mastra/core/llm";
@@ -68,8 +69,13 @@ export async function buildModel(
68
69
  overrides: BuildModelOverrides = {},
69
70
  ): Promise<MastraModelConfig> {
70
71
  void setupFetchInterceptor();
71
- const user = requestContext.get(MASTRA_USER_KEY) as User;
72
- const clientConfig = user.executionContext.client.config;
72
+ // The chat path stamps the AppKit user on the request context via
73
+ // `MastraServer`. The MCP transport routes don't thread that context
74
+ // into tool execution, so fall back to the ambient execution context
75
+ // (the active OBO scope, or the service principal) when it's absent.
76
+ const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
77
+ const executionContext = user?.executionContext ?? getExecutionContext();
78
+ const clientConfig = executionContext.client.config;
73
79
  const host = (await clientConfig.getHost()).toString();
74
80
  const headers = new Headers();
75
81
  await clientConfig.authenticate(headers);
@@ -97,7 +103,7 @@ export async function buildModel(
97
103
  const explicit = requestedClass === null ? requested : undefined;
98
104
  const modelClass = requestedClass ?? overrides.modelClass;
99
105
 
100
- const { modelId, source } = await selectModel(user.executionContext.client, host, {
106
+ const { modelId, source } = await selectModel(executionContext.client, host, {
101
107
  ...(explicit !== undefined ? { explicit } : {}),
102
108
  fuzzy: serving.fuzzy,
103
109
  threshold: serving.threshold,
package/src/plugin.ts CHANGED
@@ -26,6 +26,11 @@
26
26
  * (load + clear thread history), `/models`, `/suggestions`, and the
27
27
  * generic `/embed/:type/:id` resolver for inline chart / data
28
28
  * markers.
29
+ * - MCP: opt in with `config.mcp` to expose the agents (and optionally
30
+ * tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
31
+ * instance via `mcpServers`, so `@mastra/express` serves the stock
32
+ * MCP transport routes (`/mcp/<serverId>/...`) under the mount. See
33
+ * `./mcp.js`.
29
34
  */
30
35
 
31
36
  import {
@@ -38,7 +43,7 @@ import {
38
43
  type PluginManifest,
39
44
  type ResourceRequirement,
40
45
  } from "@databricks/appkit";
41
- import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
46
+ import { apiUtils, appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
42
47
  import type { Agent } from "@mastra/core/agent";
43
48
  import { Mastra } from "@mastra/core/mastra";
44
49
  import express from "express";
@@ -59,6 +64,7 @@ import { fetchChart } from "./chart.js";
59
64
  import type { MastraPluginConfig } from "./config.js";
60
65
  import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
61
66
  import { historyRoute } from "./history.js";
67
+ import { buildMcpServer, type ResolvedMcp } from "./mcp.js";
62
68
  import {
63
69
  createMemoryBuilder,
64
70
  createServicePrincipalPool,
@@ -67,11 +73,7 @@ import {
67
73
  import { buildObservability } from "./observability.js";
68
74
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
69
75
  import { resolveServingConfig } from "./serving.js";
70
- import {
71
- fetchStatementData,
72
- isStatementNotFoundError,
73
- STATEMENT_ROW_CAP,
74
- } from "./statement.js";
76
+ import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement.js";
75
77
 
76
78
  const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
77
79
  const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
@@ -131,6 +133,12 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
131
133
  private mastra: Mastra | null = null;
132
134
  private mastraApp: express.Express | null = null;
133
135
  private mastraServer: MastraServer | null = null;
136
+ /**
137
+ * The optional MCP server exposing this plugin's agents / tools, or
138
+ * `null` when `config.mcp` is disabled (the default). Built in
139
+ * {@link buildAgentAndServer} and registered on the Mastra instance.
140
+ */
141
+ private mcp: ResolvedMcp | null = null;
134
142
  /**
135
143
  * Dedicated service-principal Lakebase pool backing Mastra memory /
136
144
  * storage. Built once in {@link buildAgentAndServer} (outside any
@@ -207,6 +215,26 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
207
215
  (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
208
216
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
209
217
  getMastra: () => this.mastra,
218
+ /**
219
+ * MCP endpoint info when `config.mcp` is enabled, else `null`.
220
+ * Paths are absolute (under the plugin mount), ready to hand to an
221
+ * MCP client. Streamable HTTP is `http`; the SSE pair is the
222
+ * legacy transport.
223
+ */
224
+ getMcp: (): {
225
+ serverId: string;
226
+ http: string;
227
+ sse: string;
228
+ messages: string;
229
+ } | null =>
230
+ this.mcp
231
+ ? {
232
+ serverId: this.mcp.serverId,
233
+ http: `/api/${this.name}${this.mcp.httpPath}`,
234
+ sse: `/api/${this.name}${this.mcp.ssePath}`,
235
+ messages: `/api/${this.name}${this.mcp.messagePath}`,
236
+ }
237
+ : null,
210
238
  /** Express subapp Mastra is mounted on; mostly for tests. */
211
239
  getMastraServer: () => this.mastraServer,
212
240
  /**
@@ -357,7 +385,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
357
385
  // failure should leave the chat usable with a bare empty
358
386
  // state rather than surfacing a 500. Log and degrade.
359
387
  this.log.warn("suggestions:error", {
360
- error: err instanceof Error ? err.message : String(err),
388
+ error: commonUtils.errorMessage(err),
361
389
  });
362
390
  res.json({ questions: [] });
363
391
  });
@@ -448,7 +476,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
448
476
  } catch (err) {
449
477
  // The Databricks SDK throws on 404; surface as `undefined`
450
478
  // so the route maps to a clean HTTP 404 instead of a 500.
451
- if (isStatementNotFoundError(err)) return undefined;
479
+ if (apiUtils.isNotFoundError(err)) return undefined;
452
480
  throw err;
453
481
  }
454
482
  }
@@ -536,10 +564,24 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
536
564
  // `createApp`, so the OTLP endpoint / headers / sampling are
537
565
  // env-driven and shared with every other AppKit plugin.
538
566
  const observability = await buildObservability({ serviceName: this.name });
567
+ // Optional MCP exposure: build a Mastra MCP server from the
568
+ // registered agents (and, opt-in, the ambient tools) and register
569
+ // it on the Mastra instance. `@mastra/express` serves the stock MCP
570
+ // transport routes (`/mcp/<serverId>/...`) off `mcpServers`, so the
571
+ // catch-all dispatch below already routes MCP requests under OBO -
572
+ // no bespoke route needed. See `./mcp.js`.
573
+ this.mcp = buildMcpServer({
574
+ config: this.config,
575
+ pluginName: this.name,
576
+ displayName: MastraPlugin.manifest.displayName,
577
+ agents: this.built.agents,
578
+ ambientTools: this.built.ambientTools,
579
+ });
539
580
  this.mastra = new Mastra({
540
581
  agents: this.built.agents,
541
582
  ...(instanceStorage ? { storage: instanceStorage } : {}),
542
583
  ...(observability ? { observability } : {}),
584
+ ...(this.mcp ? { mcpServers: { [this.mcp.serverId]: this.mcp.server } } : {}),
543
585
  });
544
586
  this.mastraApp = express();
545
587
  attachRoutePatchMiddleware(this.mastraApp);
@@ -572,6 +614,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
572
614
  routes: ["/route/history", "/models", "/suggestions", "/embed/:type/:id"],
573
615
  instanceStorage: instanceStorage !== undefined,
574
616
  observability: observability !== undefined ? "mlflow" : "off",
617
+ mcp: this.mcp ? `/api/${this.name}${this.mcp.httpPath}` : "off",
575
618
  });
576
619
  }
577
620
  }
package/src/serving.ts CHANGED
@@ -9,6 +9,11 @@
9
9
  * onto the knobs `buildModel` and the `/models` route share.
10
10
  */
11
11
 
12
+ import {
13
+ MODEL_OVERRIDE_BODY_FIELDS,
14
+ MODEL_OVERRIDE_HEADER,
15
+ MODEL_OVERRIDE_QUERY,
16
+ } from "@dbx-tools/appkit-mastra-shared";
12
17
  import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS } from "@dbx-tools/model";
13
18
  import { stringUtils } from "@dbx-tools/shared";
14
19
 
@@ -21,15 +26,6 @@ import type { MastraPluginConfig } from "./config.js";
21
26
  */
22
27
  export const MASTRA_MODEL_OVERRIDE_KEY = "mastra__model_override";
23
28
 
24
- /** HTTP header inspected for a per-request model override. */
25
- export const MODEL_OVERRIDE_HEADER = "x-mastra-model";
26
-
27
- /** Query string parameter inspected for a per-request model override. */
28
- export const MODEL_OVERRIDE_QUERY = "model";
29
-
30
- /** Body fields (in priority order) inspected for a per-request model override. */
31
- export const MODEL_OVERRIDE_BODY_FIELDS = ["model", "modelId"] as const;
32
-
33
29
  /**
34
30
  * Minimal Express-ish request shape used by {@link extractModelOverride}.
35
31
  * Keeps this module independent of `express` so the helper can be
package/src/statement.ts CHANGED
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Databricks Statement Execution helpers for the Mastra plugin.
3
3
  *
4
- * Wraps `client.statementExecution.getStatement` with the shape,
5
- * size, and error handling the plugin's tools and the
6
- * `/embed/data/:id` route both need: a low-level fetch that returns a
7
- * raw `{columns, rows, rowCount}` shape and coerces numeric strings
8
- * to numbers so downstream charts and aggregations don't have to, a
4
+ * Wraps `client.statementExecution.getStatement` with the shape and
5
+ * size handling the plugin's tools and the `/embed/data/:id` route
6
+ * both need: a low-level fetch that returns a raw
7
+ * `{columns, rows, rowCount}` shape and coerces numeric strings to
8
+ * numbers so downstream charts and aggregations don't have to, plus a
9
9
  * hard row cap callers clamp `limit` to so a runaway result set can't
10
- * hose a response, and a structural not-found detector that
11
- * normalizes the SDK's error classes and loose `does not exist` /
12
- * `not found` message shapes into one boolean so the route can map
13
- * upstream 404s to a clean HTTP 404 without coupling to SDK
14
- * error-class identity.
10
+ * hose a response. Upstream 404s are detected via
11
+ * `apiUtils.isNotFoundError` at the call sites.
15
12
  *
16
13
  * Not Genie-specific: a Databricks `statement_id` is workspace
17
14
  * scoped and lives in the Statement Execution API regardless of
@@ -20,7 +17,7 @@
20
17
  * 404s without reaching into the Genie tool module.
21
18
  */
22
19
 
23
- import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
20
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
24
21
  import type { GenieDatasetData } from "@dbx-tools/appkit-mastra-shared";
25
22
  import { apiUtils } from "@dbx-tools/shared";
26
23
 
@@ -93,28 +90,3 @@ export async function fetchStatementData(
93
90
  rowCount: r.manifest?.total_row_count ?? dataArray.length,
94
91
  };
95
92
  }
96
-
97
- /**
98
- * True when `err` looks like the Databricks SDK's "statement not
99
- * found" error. Matches the typed {@link ApiError} 404 /
100
- * `RESOURCE_DOES_NOT_EXIST` shape first, then falls back to the
101
- * lower-level {@link HttpError} 404, then to a loose `does not
102
- * exist` / `not found` message sniff for SDK shapes we haven't
103
- * catalogued.
104
- *
105
- * Pulled into its own helper so callers (notably the
106
- * `/embed/data/:id` route) stay decoupled from SDK
107
- * error-class identity, and the conversion logic stays testable
108
- * in isolation.
109
- */
110
- export function isStatementNotFoundError(err: unknown): boolean {
111
- if (err instanceof ApiError) {
112
- if (err.statusCode === 404) return true;
113
- if (err.errorCode === "RESOURCE_DOES_NOT_EXIST") return true;
114
- }
115
- if (err instanceof HttpError && err.code === 404) return true;
116
- if (err instanceof Error && /does not exist|not found/i.test(err.message)) {
117
- return true;
118
- }
119
- return false;
120
- }