@dbx-tools/appkit-mastra 0.1.12 → 0.1.14

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 (57) hide show
  1. package/README.md +303 -637
  2. package/index.ts +46 -38
  3. package/package.json +58 -43
  4. package/src/agents.ts +224 -66
  5. package/src/chart.ts +531 -429
  6. package/src/config.ts +270 -19
  7. package/src/filesystems.ts +1090 -0
  8. package/src/genie.ts +1000 -660
  9. package/src/history.ts +166 -79
  10. package/src/mcp.ts +105 -0
  11. package/src/memory.ts +119 -81
  12. package/src/mlflow.ts +149 -0
  13. package/src/model.ts +75 -408
  14. package/src/observability.ts +144 -0
  15. package/src/pagination.ts +34 -0
  16. package/src/plugin.ts +566 -59
  17. package/src/processors.ts +168 -0
  18. package/src/rest.ts +67 -0
  19. package/src/server.ts +232 -45
  20. package/src/serving-sanitize.ts +167 -0
  21. package/src/serving.ts +27 -243
  22. package/src/statement.ts +89 -0
  23. package/src/storage-schema.ts +41 -0
  24. package/src/summarize.ts +176 -0
  25. package/src/threads.ts +338 -0
  26. package/src/workspaces.ts +346 -0
  27. package/src/writer.ts +44 -0
  28. package/tsconfig.json +41 -0
  29. package/dist/index.d.ts +0 -20
  30. package/dist/index.js +0 -20
  31. package/dist/src/agents.d.ts +0 -306
  32. package/dist/src/agents.js +0 -403
  33. package/dist/src/chart.d.ts +0 -170
  34. package/dist/src/chart.js +0 -491
  35. package/dist/src/config.d.ts +0 -183
  36. package/dist/src/config.js +0 -12
  37. package/dist/src/genie.d.ts +0 -131
  38. package/dist/src/genie.js +0 -630
  39. package/dist/src/history.d.ts +0 -67
  40. package/dist/src/history.js +0 -172
  41. package/dist/src/memory.d.ts +0 -79
  42. package/dist/src/memory.js +0 -210
  43. package/dist/src/model.d.ts +0 -159
  44. package/dist/src/model.js +0 -427
  45. package/dist/src/plugin.d.ts +0 -130
  46. package/dist/src/plugin.js +0 -261
  47. package/dist/src/processors/strip-stale-charts.d.ts +0 -29
  48. package/dist/src/processors/strip-stale-charts.js +0 -96
  49. package/dist/src/server.d.ts +0 -46
  50. package/dist/src/server.js +0 -123
  51. package/dist/src/serving.d.ts +0 -156
  52. package/dist/src/serving.js +0 -231
  53. package/dist/src/tools/email.d.ts +0 -74
  54. package/dist/src/tools/email.js +0 -122
  55. package/dist/tsconfig.build.tsbuildinfo +0 -1
  56. package/src/processors/strip-stale-charts.ts +0 -105
  57. package/src/tools/email.ts +0 -147
package/src/history.ts CHANGED
@@ -9,29 +9,29 @@
9
9
  *
10
10
  * The route is registered through {@link historyRoute} as a Mastra
11
11
  * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
- * `chatRoute`. That means the `MastraServer` auth middleware (in
13
- * `./server.ts`) has already populated `RequestContext` with
12
+ * the standard agent routes. That means the `MastraServer` auth
13
+ * middleware (in `./server.ts`) has already populated `RequestContext` with
14
14
  * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
15
  * the handler runs - no cookie or user lookups happen here, and the
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
18
 
19
- import { logUtils } from "@dbx-tools/appkit-shared";
19
+ import type {
20
+ MastraClearHistoryResponse,
21
+ MastraHistoryResponse,
22
+ MastraHistoryUIMessage,
23
+ } from "@dbx-tools/shared-mastra";
20
24
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
21
25
  import type { Agent } from "@mastra/core/agent";
22
26
  import type { MastraDBMessage } from "@mastra/core/agent/message-list";
23
- import {
24
- MASTRA_RESOURCE_ID_KEY,
25
- MASTRA_THREAD_ID_KEY,
26
- } from "@mastra/core/request-context";
27
- import { registerApiRoute } from "@mastra/core/server";
27
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
28
28
  import type { ContextWithMastra } from "@mastra/core/server";
29
- import type {
30
- MastraHistoryResponse,
31
- MastraHistoryUIMessage,
32
- } from "@dbx-tools/appkit-mastra-shared";
29
+ import { registerApiRoute } from "@mastra/core/server";
33
30
 
34
- const log = logUtils.logger("mastra/history");
31
+ import { clampPerPage, parseIntParam } from "./pagination";
32
+ import { error, log } from "@dbx-tools/shared-core";
33
+
34
+ const logger = log.logger("mastra/history");
35
35
 
36
36
  /** Default history page size; matches the Mastra storage default. */
37
37
  const DEFAULT_PER_PAGE = 20;
@@ -64,14 +64,15 @@ export interface LoadHistoryOptions {
64
64
  * so the client can prepend them above the existing transcript
65
65
  * without sorting locally.
66
66
  */
67
- export async function loadHistory(
68
- opts: LoadHistoryOptions,
69
- ): Promise<MastraHistoryResponse> {
70
- const perPage = clampPerPage(opts.perPage);
67
+ export async function loadHistory(opts: LoadHistoryOptions): Promise<MastraHistoryResponse> {
68
+ const perPage = clampPerPage(opts.perPage, {
69
+ fallback: DEFAULT_PER_PAGE,
70
+ max: MAX_PER_PAGE,
71
+ });
71
72
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
72
73
  const memory = await opts.agent.getMemory();
73
74
  if (!memory) {
74
- log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
75
+ logger.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
75
76
  return { uiMessages: [], page, perPage, total: 0, hasMore: false };
76
77
  }
77
78
  const startedAt = Date.now();
@@ -86,10 +87,8 @@ export async function loadHistory(
86
87
  },
87
88
  });
88
89
  const chronological = sortChronological(result.messages);
89
- const uiMessages = toAISdkV5Messages(
90
- chronological,
91
- ) as unknown as MastraHistoryUIMessage[];
92
- log.debug("recall:done", {
90
+ const uiMessages = toAISdkV5Messages(chronological) as unknown as MastraHistoryUIMessage[];
91
+ logger.debug("recall:done", {
93
92
  agentId: opts.agent.id,
94
93
  threadId: opts.threadId,
95
94
  page,
@@ -108,19 +107,95 @@ export async function loadHistory(
108
107
  };
109
108
  }
110
109
 
110
+ /** Inputs accepted by {@link clearHistory}. */
111
+ export interface ClearHistoryOptions {
112
+ agent: Agent;
113
+ threadId: string;
114
+ }
115
+
116
+ /**
117
+ * Wipe every persisted message tied to a thread. Returns the count
118
+ * of messages that were on the thread at delete time so the caller
119
+ * can render a "cleared N messages" affordance without an
120
+ * additional round-trip.
121
+ *
122
+ * Agents without a configured `Memory` resolve to a no-op (count
123
+ * 0), matching {@link loadHistory}'s "stateless agents return an
124
+ * empty page" stance so callers don't have to special-case them.
125
+ * Threads that don't exist yet are also a successful no-op - the
126
+ * operation is idempotent so the UI can fire-and-forget without
127
+ * tracking thread existence.
128
+ */
129
+ export async function clearHistory(opts: ClearHistoryOptions): Promise<{ cleared: number }> {
130
+ const memory = await opts.agent.getMemory();
131
+ if (!memory) {
132
+ logger.debug("clear:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
133
+ return { cleared: 0 };
134
+ }
135
+ // Mastra's `deleteThread` cascades to the message table, so we
136
+ // can't ask for a count after the fact. Read it pre-delete with a
137
+ // one-page recall sized to fit common threads in a single round
138
+ // trip; the value is for telemetry / UI, not correctness.
139
+ let cleared = 0;
140
+ try {
141
+ const probe = await memory.recall({
142
+ threadId: opts.threadId,
143
+ page: 0,
144
+ perPage: 1,
145
+ });
146
+ cleared = probe.total;
147
+ } catch (err) {
148
+ // A missing-thread error is the happy-path "nothing to count";
149
+ // every other error is logged but doesn't block the delete.
150
+ logger.debug("clear:probe-failed", {
151
+ agentId: opts.agent.id,
152
+ threadId: opts.threadId,
153
+ error: error.errorMessage(err),
154
+ });
155
+ }
156
+
157
+ const startedAt = Date.now();
158
+ try {
159
+ await memory.deleteThread(opts.threadId);
160
+ } catch (err) {
161
+ // Mastra's `deleteThread` raises when the thread row was never
162
+ // created (e.g. clearing an empty session). Surface as a soft
163
+ // warn and treat as success - the user-facing semantic is
164
+ // "history is now empty" which is already true.
165
+ logger.warn("clear:delete-soft-failed", {
166
+ agentId: opts.agent.id,
167
+ threadId: opts.threadId,
168
+ error: error.errorMessage(err),
169
+ });
170
+ }
171
+ logger.info("clear:done", {
172
+ agentId: opts.agent.id,
173
+ threadId: opts.threadId,
174
+ cleared,
175
+ elapsedMs: Date.now() - startedAt,
176
+ });
177
+ return { cleared };
178
+ }
179
+
111
180
  /** Options accepted by {@link historyRoute}. */
112
181
  export type HistoryRouteOptions =
113
- | { path: `${string}:agentId${string}`; agent?: never }
114
- | { path: string; agent: string };
182
+ { path: `${string}:agentId${string}`; agent?: never } | { path: string; agent: string };
115
183
 
116
184
  /**
117
- * Register a `GET <path>` Mastra custom API route that returns a page
118
- * of AI SDK V5 `UIMessage`s for the caller's current thread.
185
+ * Register the `<path>` Mastra custom API route. Handles two
186
+ * methods on the same mount:
119
187
  *
120
- * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
121
- * fixed-agent mount, or include `:agentId` in the path for dynamic
122
- * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
123
- * layout (`/route/chat` + `/route/chat/:agentId`).
188
+ * - `GET`: return a page of AI SDK V5 `UIMessage`s for the
189
+ * caller's current thread ({@link loadHistory}).
190
+ * - `DELETE`: wipe every persisted message on the caller's
191
+ * thread ({@link clearHistory}). The session cookie that
192
+ * anchors the thread id is left alone so the user keeps the
193
+ * same thread - only the contents go away.
194
+ *
195
+ * Follows the `@mastra/ai-sdk` `chatRoute` convention for agent
196
+ * binding: pass `agent` for a fixed-agent mount, or include
197
+ * `:agentId` in the path for dynamic routing. The plugin registers
198
+ * both `/route/history` (default agent) and `/route/history/:agentId`.
124
199
  *
125
200
  * The handler reads `threadId` and `resourceId` from `RequestContext`
126
201
  * (populated upstream by `MastraServer.registerAuthMiddleware`), so
@@ -134,44 +209,68 @@ export function historyRoute(options: HistoryRouteOptions) {
134
209
  "historyRoute path must include `:agentId` or `agent` must be passed explicitly",
135
210
  );
136
211
  }
137
- return registerApiRoute(path, {
138
- method: "GET",
139
- handler: async (c: ContextWithMastra) => {
140
- const mastra = c.get("mastra");
141
- const requestContext = c.get("requestContext");
142
- const agentId = fixedAgent ?? c.req.param("agentId");
143
- if (!agentId) {
144
- return c.json({ error: "agentId is required" }, 400);
145
- }
146
- const agent = mastra.getAgentById(agentId);
147
- if (!agent) {
148
- return c.json({ error: `Unknown agent "${agentId}"` }, 404);
149
- }
150
- const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
151
- if (!threadId) {
152
- return c.json({ error: "thread id missing from request context" }, 400);
153
- }
154
- const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY) as
155
- | string
156
- | undefined;
157
- const payload = await loadHistory({
158
- agent,
159
- threadId,
160
- ...(resourceId ? { resourceId } : {}),
161
- page: parseIntParam(c.req.query("page")),
162
- perPage: parseIntParam(c.req.query("perPage")),
163
- });
164
- return c.json(payload);
165
- },
166
- });
167
- }
212
+ // Tiny resolver shared by GET / DELETE: derive the active agent
213
+ // and thread id, returning a JSON error response when either is
214
+ // missing. Keeps both handlers thin and gives them identical
215
+ // validation behaviour.
216
+ const resolveContext = (c: ContextWithMastra) => {
217
+ const mastra = c.get("mastra");
218
+ const requestContext = c.get("requestContext");
219
+ const agentId = fixedAgent ?? c.req.param("agentId");
220
+ if (!agentId) {
221
+ return { error: c.json({ error: "agentId is required" }, 400) } as const;
222
+ }
223
+ const agent = mastra.getAgentById(agentId);
224
+ if (!agent) {
225
+ return {
226
+ error: c.json({ error: `Unknown agent "${agentId}"` }, 404),
227
+ } as const;
228
+ }
229
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
230
+ if (!threadId) {
231
+ return {
232
+ error: c.json({ error: "thread id missing from request context" }, 400),
233
+ } as const;
234
+ }
235
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY) as string | undefined;
236
+ return { agentId, agent, threadId, resourceId } as const;
237
+ };
168
238
 
169
- /** Coerce / clamp `perPage`; falls back to the page-size default. */
170
- function clampPerPage(value: number | undefined): number {
171
- if (value === undefined || Number.isNaN(value)) return DEFAULT_PER_PAGE;
172
- const n = Math.trunc(value);
173
- if (n <= 0) return DEFAULT_PER_PAGE;
174
- return Math.min(n, MAX_PER_PAGE);
239
+ return [
240
+ registerApiRoute(path, {
241
+ method: "GET",
242
+ handler: async (c: ContextWithMastra) => {
243
+ const ctx = resolveContext(c);
244
+ if ("error" in ctx) return ctx.error;
245
+ const payload = await loadHistory({
246
+ agent: ctx.agent,
247
+ threadId: ctx.threadId,
248
+ ...(ctx.resourceId ? { resourceId: ctx.resourceId } : {}),
249
+ page: parseIntParam(c.req.query("page")),
250
+ perPage: parseIntParam(c.req.query("perPage")),
251
+ });
252
+ return c.json(payload);
253
+ },
254
+ }),
255
+ registerApiRoute(path, {
256
+ method: "DELETE",
257
+ handler: async (c: ContextWithMastra) => {
258
+ const ctx = resolveContext(c);
259
+ if ("error" in ctx) return ctx.error;
260
+ const { cleared } = await clearHistory({
261
+ agent: ctx.agent,
262
+ threadId: ctx.threadId,
263
+ });
264
+ const payload: MastraClearHistoryResponse = {
265
+ ok: true,
266
+ agentId: ctx.agentId,
267
+ threadId: ctx.threadId,
268
+ cleared,
269
+ };
270
+ return c.json(payload);
271
+ },
272
+ }),
273
+ ];
175
274
  }
176
275
 
177
276
  /**
@@ -196,15 +295,3 @@ function toEpoch(value: unknown): number {
196
295
  }
197
296
  return 0;
198
297
  }
199
-
200
- /**
201
- * Coerce a Hono query value into a non-negative integer. Returns
202
- * `undefined` for empty / non-numeric / negative inputs so
203
- * {@link loadHistory} can apply its built-in defaults.
204
- */
205
- function parseIntParam(value: string | undefined): number | undefined {
206
- if (!value) return undefined;
207
- const n = Number(value);
208
- if (!Number.isFinite(n) || n < 0) return undefined;
209
- return Math.trunc(n);
210
- }
package/src/mcp.ts ADDED
@@ -0,0 +1,105 @@
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";
17
+ import type { MastraMcpConfig, MastraPluginConfig } from "./config";
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>`).
25
+ *
26
+ * The paths are the **clean public aliases** (`/mcp`, `/sse`,
27
+ * `/messages`) the plugin exposes. `@mastra/express` actually mounts the
28
+ * transports under `/mcp/<serverId>/<transport>` off the `Mastra`
29
+ * instance's `mcpServers`; the plugin rewrites the alias to that route
30
+ * before dispatch (see {@link ResolvedMcp.serverId}), so a client never
31
+ * sees the doubled `/mcp/<serverId>/mcp` segment.
32
+ */
33
+ export interface ResolvedMcp {
34
+ /**
35
+ * Registry id, used in the underlying `@mastra/express` route
36
+ * (`/mcp/<serverId>/...`) the plugin rewrites the clean alias to.
37
+ */
38
+ serverId: string;
39
+ /** The Mastra MCP server to hand to `new Mastra({ mcpServers })`. */
40
+ server: MCPServer;
41
+ /** Streamable-HTTP transport path, relative to the plugin base path. */
42
+ httpPath: string;
43
+ /** SSE transport path, relative to the plugin base path. */
44
+ ssePath: string;
45
+ /** SSE message path, relative to the plugin base path. */
46
+ messagePath: string;
47
+ }
48
+
49
+ /**
50
+ * Build the plugin's MCP server, or `null` only when `config.mcp` is
51
+ * explicitly `false`.
52
+ *
53
+ * Agent exposure is on by default because it's cheap: the already-
54
+ * registered agents are wrapped as `ask_<agentId>` tools and handed to
55
+ * `@mastra/express` (already mounted for the chat routes) - no extra
56
+ * process, dependency, or network cost, and requests run under the same
57
+ * OBO scope. So `undefined` (the default) and `true` both expose every
58
+ * registered agent under a server id equal to the plugin name; only
59
+ * `false` turns MCP off. The object form ({@link MastraMcpConfig}) tunes
60
+ * the id / advertised metadata and can additionally expose the plugin's
61
+ * ambient tools or a set of extra MCP-only tools. Ambient tools stay off
62
+ * unless explicitly enabled - they assume an in-process chat turn, so
63
+ * they aren't cheap / safe to expose to a standalone MCP client.
64
+ */
65
+ export function buildMcpServer(opts: {
66
+ config: MastraPluginConfig;
67
+ pluginName: string;
68
+ displayName: string;
69
+ agents: Record<string, Agent>;
70
+ ambientTools: MastraTools;
71
+ }): ResolvedMcp | null {
72
+ const { config, pluginName, displayName, agents, ambientTools } = opts;
73
+ if (config.mcp === false) return null;
74
+ const settings: MastraMcpConfig = typeof config.mcp === "object" ? config.mcp : {};
75
+
76
+ const serverId = settings.serverId ?? pluginName;
77
+ const exposeAgents = settings.agents !== false;
78
+ const exposeTools = settings.tools === true;
79
+
80
+ // MCPServerConfig.tools is required, so always pass a record (empty
81
+ // when the caller exposes only agents).
82
+ const tools: MastraTools = {
83
+ ...(exposeTools ? ambientTools : {}),
84
+ ...(settings.extraTools ?? {}),
85
+ };
86
+
87
+ const server = new MCPServer({
88
+ id: serverId,
89
+ name: settings.name ?? `${displayName} MCP`,
90
+ version: settings.version ?? DEFAULT_MCP_VERSION,
91
+ ...(settings.description ? { description: settings.description } : {}),
92
+ ...(exposeAgents ? { agents } : {}),
93
+ tools,
94
+ });
95
+
96
+ // Advertise the clean aliases; the plugin rewrites these to the stock
97
+ // `/mcp/<serverId>/<transport>` routes `@mastra/express` mounts.
98
+ return {
99
+ serverId,
100
+ server,
101
+ httpPath: "/mcp",
102
+ ssePath: "/sse",
103
+ messagePath: "/messages",
104
+ };
105
+ }