Skip to content
Closed
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
59 changes: 59 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,51 @@ export async function handleChatCore({
};
let tokensCompressed: number | null = null;
body = injectSystemPrompt(body);

// ── Plugin onRequest hook ──
try {
const { runOnRequest } = await import("@/lib/plugins/index");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Dynamic imports inside the main request handler (handleChatCore) introduce unnecessary overhead on every request. Consider moving this to a top-level import or implementing a lazy-loading pattern that caches the module after the first load to improve performance in this hot path.

const pluginCtx = {
requestId: traceId,
body,
model,
provider,
apiKeyInfo,
metadata: {},
};
const pluginResult = await runOnRequest(pluginCtx);
if (pluginResult?.blocked) {
log?.info?.("PLUGIN", `Request blocked by plugin`);
return {
success: false,
status: 403,
error: "Request blocked by plugin",
response: pluginResult.response
? new Response(JSON.stringify(pluginResult.response), {
status: 403,
headers: { "Content-Type": "application/json" },
})
: new Response(
JSON.stringify({
error: { message: "Request blocked by plugin", type: "plugin_block" },
}),
{
status: 403,
headers: { "Content-Type": "application/json" },
}
),
};
}
if (pluginResult?.ctx && "body" in pluginResult.ctx) {
body = (pluginResult.ctx as Record<string, unknown>).body;
}
} catch (pluginErr) {
log?.debug?.(
"PLUGIN",
`onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}`
);
}

let effectiveServiceTier: "standard" | "priority" = "standard";
const resolveEffectiveServiceTier = (requestBody?: unknown): "standard" | "priority" => {
if (provider !== "codex") return "standard";
Expand Down Expand Up @@ -2758,6 +2803,20 @@ export async function handleChatCore({
);
}
} catch (error) {
// ── Plugin onError hook ──
try {
const { runOnError } = await import("@/lib/plugins/index");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Dynamic import of the plugin module inside the error handler. Similar to the onRequest hook, this should be optimized to avoid per-request overhead.

await runOnError(
{ requestId: traceId, body, model, provider, apiKeyInfo, metadata: {} },
error instanceof Error ? error : new Error(String(error))
);
} catch (pluginErr) {
log?.debug?.(
"PLUGIN",
`onError hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}`
);
}

const parsedStatus = Number(error?.statusCode);
const statusCode =
Number.isInteger(parsedStatus) && parsedStatus >= 400 && parsedStatus <= 599
Expand Down
29 changes: 28 additions & 1 deletion open-sse/mcp-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
} from "./tools/advancedTools.ts";
import { memoryTools } from "./tools/memoryTools.ts";
import { skillTools } from "./tools/skillTools.ts";
import { pluginTools } from "./tools/pluginTools.ts";
import { compressionTools } from "./tools/compressionTools.ts";
import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts";
import { smartFilterText } from "../services/compression/engines/mcpAccessibility/index.ts";
Expand All @@ -98,7 +99,10 @@ const MCP_ALLOWED_SCOPES = new Set(
.filter(Boolean)
);
const TOTAL_MCP_TOOL_COUNT =
MCP_TOOLS.length + Object.keys(memoryTools).length + Object.keys(skillTools).length;
MCP_TOOLS.length +
Object.keys(memoryTools).length +
Object.keys(skillTools).length +
pluginTools.length;

type JsonRecord = Record<string, unknown>;

Expand Down Expand Up @@ -999,6 +1003,29 @@ export function createMcpServer(): McpServer {
);
});

// ── Plugin Tools ──────────────────────────────
pluginTools.forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
// @ts-ignore: dynamic zod access
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
});

// ── Compression Tools ─────────────────────────
Object.values(compressionTools).forEach((toolDef) => {
server.registerTool(
Expand Down
149 changes: 149 additions & 0 deletions open-sse/mcp-server/tools/pluginTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* MCP Plugin Tools — 8 tools for plugin management.
*
* @module mcp-server/tools/pluginTools
*/

import { z } from "zod";
import { listPlugins, getPluginByName, updatePluginConfig } from "../../../src/lib/db/plugins";
import { pluginManager } from "../../../src/lib/plugins/manager";

export const pluginTools = [
{
name: "plugin_list",
description: "List all installed plugins with their status, hooks, and metadata.",
inputSchema: z.object({
status: z
.enum(["installed", "active", "inactive", "error"])
.optional()
.describe("Filter by plugin status"),
}),
handler: async (args: { status?: string }) => {
const plugins = listPlugins(args.status as any);
return {
plugins: plugins.map((p) => ({
name: p.name,
version: p.version,
description: p.description,
status: p.status,
enabled: p.enabled === 1,
hooks: JSON.parse(p.hooks || "[]"),
permissions: JSON.parse(p.permissions || "[]"),
installedAt: p.installedAt,
activatedAt: p.activatedAt,
})),
};
},
},

{
name: "plugin_install",
description: "Install a plugin from a local directory path.",
inputSchema: z.object({
path: z.string().describe("Absolute path to the plugin directory containing plugin.json"),
}),
handler: async (args: { path: string }) => {
const plugin = await pluginManager.install(args.path);
return {
success: true,
plugin: {
name: plugin.name,
version: plugin.version,
status: plugin.status,
},
};
},
},

{
name: "plugin_activate",
description: "Activate an installed plugin (loads hooks into the request pipeline).",
inputSchema: z.object({
name: z.string().describe("Plugin name (kebab-case)"),
}),
handler: async (args: { name: string }) => {
await pluginManager.activate(args.name);
return { success: true, message: `Plugin '${args.name}' activated` };
},
},

{
name: "plugin_deactivate",
description: "Deactivate an active plugin (unloads hooks from the request pipeline).",
inputSchema: z.object({
name: z.string().describe("Plugin name (kebab-case)"),
}),
handler: async (args: { name: string }) => {
await pluginManager.deactivate(args.name);
return { success: true, message: `Plugin '${args.name}' deactivated` };
},
},

{
name: "plugin_uninstall",
description: "Uninstall a plugin (deactivates, removes files, removes from DB).",
inputSchema: z.object({
name: z.string().describe("Plugin name (kebab-case)"),
}),
handler: async (args: { name: string }) => {
await pluginManager.uninstall(args.name);
return { success: true, message: `Plugin '${args.name}' uninstalled` };
},
},

{
name: "plugin_configure",
description: "Get or update a plugin's configuration.",
inputSchema: z.object({
name: z.string().describe("Plugin name"),
config: z
.record(z.string(), z.unknown())
.optional()
.describe("New config values to merge (omit to just read current config)"),
}),
handler: async (args: { name: string; config?: Record<string, unknown> }) => {
const plugin = getPluginByName(args.name);
if (!plugin) throw new Error(`Plugin '${args.name}' not found`);

if (args.config) {
const current = JSON.parse(plugin.config || "{}");
const merged = { ...current, ...args.config };
updatePluginConfig(args.name, merged);
return { success: true, config: merged };
}

return {
config: JSON.parse(plugin.config || "{}"),
configSchema: JSON.parse(plugin.configSchema || "{}"),
};
},
},

{
name: "plugin_executions",
description: "View plugin execution history (from skill_executions table).",
inputSchema: z.object({
name: z.string().optional().describe("Filter by plugin name"),
limit: z.number().min(1).max(100).default(20).describe("Max results to return"),
}),
handler: async (args: { name?: string; limit?: number }) => {
// Plugin executions are tracked via the skills system
const { skillExecutor } = await import("../../../src/lib/skills/executor");
const executions = skillExecutor.listExecutions(undefined, args.limit || 20);
return { executions };
},
},

{
name: "plugin_scan",
description: "Scan the plugin directory for new plugins and sync with DB.",
inputSchema: z.object({}),
handler: async () => {
const result = await pluginManager.scan();
return {
discovered: result.discovered,
errors: result.errors,
};
},
},
];
Loading