Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/mesh/migrations/035-reports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Reports Migration
*
* Creates the reports table for storing automated reports (performance audits,
* security scans, collection reorder rankings, etc.) served via REPORTS_BINDING.
*/

import { Kysely } from "kysely";

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable("reports")
.addColumn("id", "text", (col) => col.primaryKey())
.addColumn("organization_id", "text", (col) =>
col.notNull().references("organization.id").onDelete("cascade"),
)
.addColumn("title", "text", (col) => col.notNull())
.addColumn("category", "text", (col) => col.notNull())
.addColumn("status", "text", (col) => col.notNull())
.addColumn("summary", "text", (col) => col.notNull())
.addColumn("source", "text")
.addColumn("tags", "text")
.addColumn("lifecycle_status", "text", (col) =>
col.notNull().defaultTo("unread"),
)
.addColumn("sections", "text", (col) => col.notNull())
.addColumn("created_at", "text", (col) => col.notNull())
.addColumn("updated_at", "text", (col) => col.notNull())
.execute();

await db.schema
.createIndex("reports_organization_id")
.on("reports")
.columns(["organization_id"])
.execute();
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex("reports_organization_id").execute();
await db.schema.dropTable("reports").execute();
}
2 changes: 2 additions & 0 deletions apps/mesh/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as migration031adddependencymode from "./031-add-dependency-mode.ts";
import * as migration032projects from "./032-projects.ts";
import * as migration033threadstatus from "./033-thread-status.ts";
import * as migration034monitoringdashboards from "./034-monitoring-dashboards.ts";
import * as migration035reports from "./035-reports.ts";

/**
* Core migrations for the Mesh application.
Expand Down Expand Up @@ -78,6 +79,7 @@ const migrations: Record<string, Migration> = {
"032-projects": migration032projects,
"033-thread-status": migration033threadstatus,
"034-monitoring-dashboards": migration034monitoringdashboards,
"035-reports": migration035reports,
};

export default migrations;
2 changes: 2 additions & 0 deletions apps/mesh/src/core/context-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { SqlMonitoringDashboardStorage } from "../storage/monitoring-dashboards"
import { OrganizationSettingsStorage } from "../storage/organization-settings";
import { ProjectsStorage } from "../storage/projects";
import { ProjectPluginConfigsStorage } from "../storage/project-plugin-configs";
import { KyselyReportsStorage } from "../storage/reports";
import { TagStorage } from "../storage/tags";
import type { Database, Permission } from "../storage/types";
import { UserStorage } from "../storage/user";
Expand Down Expand Up @@ -747,6 +748,7 @@ export async function createMeshContextFactory(
tags: new TagStorage(config.db),
projects: new ProjectsStorage(config.db),
projectPluginConfigs: new ProjectPluginConfigsStorage(config.db),
reports: new KyselyReportsStorage(config.db),
// Note: Organizations, teams, members, roles managed by Better Auth organization plugin
// Note: Policies handled by Better Auth permissions directly
// Note: API keys (tokens) managed by Better Auth API Key plugin
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/core/define-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const createMockContext = (): MeshContext => ({
projects: null as never,
projectPluginConfigs: null as never,
monitoringDashboards: null as never,
reports: null as never,
},
vault: null as never,
authInstance: null as never,
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/core/mesh-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const createMockContext = (overrides?: Partial<MeshContext>): MeshContext => ({
projects: null as never,
projectPluginConfigs: null as never,
monitoringDashboards: null as never,
reports: null as never,
},
vault: null as never,
authInstance: null as never,
Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/core/mesh-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ import type { UserStorage } from "../storage/user";
import type { VirtualMCPStorage } from "../storage/virtual";
import type { ProjectsStorage } from "../storage/projects";
import type { ProjectPluginConfigsStorage } from "../storage/project-plugin-configs";
import type { ReportsStorage } from "../storage/reports";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";

Expand Down Expand Up @@ -260,6 +261,7 @@ export interface MeshStorage {
tags: TagStorage;
projects: ProjectsStorage;
projectPluginConfigs: ProjectPluginConfigsStorage;
reports: ReportsStorage;
}

// ============================================================================
Expand Down
42 changes: 36 additions & 6 deletions apps/mesh/src/mcp-clients/virtual-mcp/passthrough-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,16 @@ interface PromptCache extends Cache<Prompt> {}
* actually needs it (e.g. `callTool`).
*
* This avoids the ~80-120ms MCP handshake per connection when tools are cached.
*
* @param requiredTools - Tool names that must be present in the cache for it to
* be considered valid. If any are missing the client fetches live to handle
* stale caches (e.g. after new management tools are deployed).
*/
function createLazyClient(
connection: ConnectionEntity,
ctx: MeshContext,
superUser: boolean,
requiredTools?: string[],
): Client {
// Placeholder client — never connects to anything
const placeholder = new Client(
Expand Down Expand Up @@ -105,15 +110,27 @@ function createLazyClient(
return realClientPromise;
}

const hasCachedTools =
const cachedTools =
connection.connection_type !== "VIRTUAL" &&
Array.isArray(connection.tools) &&
connection.tools.length > 0;
connection.tools.length > 0
? connection.tools
: null;

// Cache is valid only if it contains every tool that will be needed.
// If `requiredTools` are specified and any are absent, the DB cache is stale
// (e.g. new management tools were deployed since the cache was written).
// In that case fall back to a live fetch so callers never get an empty list.
const cacheHasAllRequired =
!requiredTools?.length ||
requiredTools.every((name) => cachedTools?.some((t) => t.name === name));

const hasCachedTools = cachedTools !== null && cacheHasAllRequired;

// If cached tools exist, listTools returns them without connecting
if (hasCachedTools) {
placeholder.listTools = async () => ({
tools: connection.tools!.map((tool) => ({
tools: cachedTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema as Tool["inputSchema"],
Expand All @@ -123,7 +140,7 @@ function createLazyClient(
})),
});
} else {
// No cached tools — must connect to get tool list
// No cached tools (or stale cache) — must connect to get tool list
placeholder.listTools = async () => {
const real = await getRealClient();
return real.listTools();
Expand Down Expand Up @@ -198,16 +215,26 @@ function createLazyClient(
* Creates lazy-connecting clients for all connections. Clients with cached
* tools in the database will skip the MCP handshake entirely during tool
* listing, only connecting when a tool is actually called.
*
* @param selectionMap - Optional map of connection ID to selected tool names.
* Used to validate that the DB cache contains every required tool; if not,
* the client will fetch live instead.
*/
function createClientMap(
connections: ConnectionEntity[],
ctx: MeshContext,
superUser = false,
selectionMap?: Map<string, VirtualMCPConnection>,
): Map<string, Client> {
const clientMap = new Map<string, Client>();

for (const connection of connections) {
clientMap.set(connection.id, createLazyClient(connection, ctx, superUser));
const requiredTools =
selectionMap?.get(connection.id)?.selected_tools ?? undefined;
clientMap.set(
connection.id,
createLazyClient(connection, ctx, superUser, requiredTools),
);
}

return clientMap;
Expand Down Expand Up @@ -272,11 +299,14 @@ export class PassthroughClient extends Client {
this._connections.set(connection.id, connection);
}

// Create lazy-connecting client map (synchronous — no connections established yet)
// Create lazy-connecting client map (synchronous — no connections established yet).
// Pass the selection map so each client can detect a stale DB cache (e.g. when
// new management tools were deployed and the _self cache hasn't been refreshed yet).
this._clients = createClientMap(
this.options.connections,
this.ctx,
this.options.superUser,
this._selectionMap,
);

// Initialize lazy caches - all share the same ProxyCollection
Expand Down
3 changes: 2 additions & 1 deletion apps/mesh/src/shared/utils/generate-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type IdPrefixes =
| "mtag"
| "proj"
| "ppc"
| "dash";
| "dash"
| "rpt";

export function generatePrefixedId(prefix: IdPrefixes) {
return `${prefix}_${nanoid()}`;
Expand Down
Loading