Enable plugins to declare per-project MCP server registrations. - Add plugin MCP server contribution types, loading, and resolution across core and engine runtimes. - Expose resolved plugin registrations through project configuration APIs and MCP settings UI. - Document the declarative contribution API and add release metadata and regression coverage. Files changed: .changeset/plugin-mcp-servers.md | 7 ++ docs/PLUGIN_AUTHORING.md | 17 +++++ docs/mcp.md | 4 ++ docs/settings-reference.md | 4 ++ packages/core/src/__tests__/mcp-config.test.ts | 33 +++++++++ .../__tests__/plugin-contribution-types.test.ts | 16 +++++ .../__tests__/plugin-loader-single-load.test.ts | 23 +++++++ packages/core/src/index.gate.ts | 3 + packages/core/src/index.ts | 3 + packages/core/src/mcp-config.ts | 37 ++++++++-- packages/core/src/plugin-loader.ts | 24 +++++++ packages/core/src/plugin-mcp-servers.ts | 78 ++++++++++++++++++++++ packages/core/src/plugin-types.ts | 15 ++++- packages/core/src/types.ts | 2 +- .../__tests__/SettingsModal.mcp.test.tsx | 34 +++++++++- .../settings/sections/McpServersCard.tsx | 52 ++++++++++----- .../settings/sections/ProjectMcpSection.tsx | 31 ++++++++- .../register-config-mcp-pi-settings-routes.test.ts | 18 ++++- packages/dashboard/src/routes/context.ts | 71 +++++++++++++++++++- .../register-config-mcp-pi-settings-routes.ts | 30 ++++++++- .../engine/src/__tests__/mcp-resolution.test.ts | 20 ++++++ packages/engine/src/mcp-resolution.ts | 15 ++++- packages/engine/src/plugin-runner.ts | 38 +++++++++++ packages/engine/src/runtimes/in-process-runtime.ts | 23 +++++++ packages/plugin-sdk/src/index.ts | 1 + 25 files changed, 563 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-8491 Fusion-Task-Lineage: be7e22fa-5776-4b3d-9fd1-a873799e6427 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
283 lines
8.7 KiB
TypeScript
283 lines
8.7 KiB
TypeScript
/**
|
|
* Fusion Plugin SDK
|
|
*
|
|
* This package provides type definitions and helpers for creating Fusion plugins.
|
|
* It re-exports all plugin-related types from @fusion/core.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { definePlugin } from "@fusion/plugin-sdk";
|
|
*
|
|
* export default definePlugin({
|
|
* manifest: {
|
|
* id: "my-plugin",
|
|
* name: "My Plugin",
|
|
* version: "1.0.0",
|
|
* },
|
|
* hooks: {
|
|
* onLoad: async (ctx) => {
|
|
* ctx.logger.info("Plugin loaded!");
|
|
* },
|
|
* },
|
|
* tools: [
|
|
* {
|
|
* name: "my_tool",
|
|
* description: "Does something useful",
|
|
* parameters: { type: "object", properties: { input: { type: "string" } } },
|
|
* execute: async (params, ctx) => ({
|
|
* content: [{ type: "text", text: `Processed: ${params.input}` }],
|
|
* }),
|
|
* },
|
|
* ],
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
// Re-export all plugin types from @fusion/core
|
|
export type {
|
|
PluginManifest,
|
|
PluginSettingSchema,
|
|
PluginSettingType,
|
|
PluginOnLoad,
|
|
PluginOnUnload,
|
|
PluginOnSchemaInit,
|
|
PluginOnPostgresSchemaInit,
|
|
PluginPostgresSchemaDefinition,
|
|
PluginOnTaskCreated,
|
|
PluginOnTaskMoved,
|
|
PluginOnTaskCompleted,
|
|
PluginOnError,
|
|
PluginToolDefinition,
|
|
PluginToolResult,
|
|
PluginRouteDefinition,
|
|
PluginRouteMethod,
|
|
PluginRouteResponse,
|
|
PluginRouteResult,
|
|
PluginUiSurface,
|
|
PluginUiSlotDefinition,
|
|
PluginUiContributionSurface,
|
|
PluginUiContributionWhen,
|
|
PluginUiActionDescriptor,
|
|
SettingsProviderCardContribution,
|
|
SettingsConfigSectionContribution,
|
|
OnboardingProviderCardContribution,
|
|
OnboardingSetupHelpContribution,
|
|
OnboardingProviderRecommendationContribution,
|
|
PostOnboardingRecommendationContribution,
|
|
PluginUiContributionDefinition,
|
|
PluginUiContributionInputDefinition,
|
|
PluginDashboardViewDefinition,
|
|
PluginRuntimeManifestMetadata,
|
|
PluginRuntimeFactory,
|
|
PluginRuntimeRegistration,
|
|
CliProviderType,
|
|
CliProviderActionMetadata,
|
|
CliProviderProbeResult,
|
|
CliProviderModelDiscoveryResult,
|
|
CliProviderRuntimeRegistration,
|
|
CliProviderContribution,
|
|
PluginContext,
|
|
PluginLogger,
|
|
PluginSkillContribution,
|
|
PluginMcpServerContribution,
|
|
PluginWorkflowStepContribution,
|
|
PluginTraitContribution,
|
|
PluginTraitHookDescriptor,
|
|
PluginTraitFlags,
|
|
WorkflowExtensionContribution,
|
|
WorkflowExtensionMetadata,
|
|
WorkflowExtensionBaseContribution,
|
|
WorkflowColumnMetadataExtensionContribution,
|
|
WorkflowMovePolicyExtensionContribution,
|
|
WorkflowWorkEngineExtensionContribution,
|
|
WorkflowNodeHandlerExtensionContribution,
|
|
TaskVerdictProviderExtensionContribution,
|
|
AutoMergeFactProviderExtensionContribution,
|
|
WorkflowExtensionConfigField,
|
|
WorkflowExtensionConfigSchema,
|
|
WorkflowExtensionFallback,
|
|
WorkflowExtensionKind,
|
|
WorkflowMovePolicyDecision,
|
|
WorkflowMovePolicyInput,
|
|
WorkflowMovePolicyHandler,
|
|
WorkflowWorkEngineDispatchResult,
|
|
WorkflowWorkEngineInput,
|
|
WorkflowWorkEngineHandler,
|
|
WorkflowNodeExtensionResult,
|
|
WorkflowNodeHandlerInput,
|
|
WorkflowNodeExtensionHandler,
|
|
TaskVerdictStatus,
|
|
TaskVerdictProviderInput,
|
|
TaskVerdictProviderResult,
|
|
TaskVerdictProviderHandler,
|
|
AutoMergeRoute,
|
|
AutoMergeFactProviderInput,
|
|
AutoMergeFactProviderResult,
|
|
AutoMergeFactProviderHandler,
|
|
PluginPromptSurface,
|
|
/**
|
|
* FNXC:PluginPrompt 2026-07-10-00:00:
|
|
* Re-export the core-authored PluginPromptContribution type so SDK consumers see the enforced `condition` grammar in generated declarations.
|
|
*/
|
|
PluginPromptContribution,
|
|
PluginPromptContributions,
|
|
ExecutorRuntimeTaskContext,
|
|
ExecutorRuntimeEnvContribution,
|
|
PluginExecutorRuntimeEnvHook,
|
|
PluginSetupStatus,
|
|
PluginSetupCheckResult,
|
|
PluginSetupHooks,
|
|
PluginSetupManifest,
|
|
FusionPlugin,
|
|
PluginState,
|
|
PluginInstallation,
|
|
BoardActionServices,
|
|
BoardActionTaskStore,
|
|
MoveBoardTaskInput,
|
|
UpdateBoardTaskInput,
|
|
} from "@fusion/core";
|
|
|
|
export {
|
|
WORKFLOW_EXTENSION_SCHEMA_VERSION,
|
|
workflowExtensionRegistryId,
|
|
createBoardActionServices,
|
|
} from "@fusion/core";
|
|
|
|
// ── Step-inversion IR types (type-only) ──────────────────────────────────────
|
|
// TYPE-ONLY re-exports of the workflow-modelable step constructs (KTD-3/12/13/15)
|
|
// so plugin authors can author/validate workflow IR and step parsers against the
|
|
// canonical shapes. These are erased at build time, so the standalone plugin-sdk
|
|
// artifact carries no @fusion runtime specifiers (see cli plugin-sdk-export test).
|
|
export type {
|
|
// Graph IR primitives.
|
|
WorkflowIr,
|
|
WorkflowIrV1,
|
|
WorkflowIrV2,
|
|
WorkflowIrNode,
|
|
WorkflowIrEdge,
|
|
WorkflowIrNodeKind,
|
|
// Columns + per-column permanent-agent binding (column-agent plan KTD-1, R12).
|
|
WorkflowIrColumn,
|
|
WorkflowIrColumnTrait,
|
|
WorkflowColumnAgent,
|
|
// Foreach / artifacts / custom fields (step inversion).
|
|
WorkflowForeachConfig,
|
|
WorkflowLoopConfig,
|
|
WorkflowLoopExitCondition,
|
|
WorkflowIrArtifact,
|
|
WorkflowFieldDefinition,
|
|
WorkflowFieldType,
|
|
WorkflowFieldOption,
|
|
WorkflowFieldRender,
|
|
// Workflow settings (typed, workflow-declared policy schema; values persist
|
|
// per-(workflowId, projectId) — mirrors the custom-field surface one level up).
|
|
WorkflowSettingDefinition,
|
|
WorkflowSettingType,
|
|
WorkflowSettingOption,
|
|
WorkflowSettingRender,
|
|
// Step-parser contract.
|
|
StepParser,
|
|
StepParseResult,
|
|
ParsedStep,
|
|
} from "@fusion/core";
|
|
|
|
import type { StepParseResult } from "@fusion/core";
|
|
|
|
/**
|
|
* A plugin's step-parser contribution (KTD-12). A plugin's runtime loader returns
|
|
* these from its `getPluginStepParsers` getter; the engine wraps each fail-closed
|
|
* and registers it under `plugin:<pluginId>:<parserId>`. `parse` is SYNCHRONOUS
|
|
* (project-local trust tier) and may throw on malformed input — a throw maps to a
|
|
* routable `outcome:parse-error`.
|
|
*
|
|
* Structurally identical to the engine-side `PluginStepParserContribution` the
|
|
* plugin runner consumes; defined here so plugin authors do not depend on the
|
|
* engine package (the SDK depends on @fusion/core only).
|
|
*/
|
|
export interface PluginStepParserContribution {
|
|
parserId: string;
|
|
parse: (content: string) => StepParseResult;
|
|
}
|
|
|
|
import type { FusionPlugin } from "@fusion/core";
|
|
|
|
// NOTE (U8): trait-contribution VALIDATION lives in @fusion/core
|
|
// (validatePluginTraitContribution) and runs engine-side at registration.
|
|
// It is deliberately NOT re-exported here — plugin-sdk's built artifact must
|
|
// carry no @fusion runtime specifiers (see cli plugin-sdk-export test); only
|
|
// type-level re-exports are allowed from @fusion/core.
|
|
|
|
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
|
export function validatePluginManifest(manifest: unknown): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
if (manifest === null || manifest === undefined) {
|
|
return { valid: false, errors: ["Manifest is required"] };
|
|
}
|
|
|
|
if (typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
return { valid: false, errors: ["Manifest must be an object"] };
|
|
}
|
|
|
|
const m = manifest as Record<string, unknown>;
|
|
|
|
if (!m.id || typeof m.id !== "string" || m.id.trim() === "") {
|
|
errors.push("id is required and must be a non-empty string");
|
|
} else if (!SLUG_PATTERN.test(m.id)) {
|
|
errors.push("id must be a valid slug (lowercase, alphanumeric, hyphens only, cannot start or end with hyphen)");
|
|
}
|
|
|
|
if (!m.name || typeof m.name !== "string" || m.name.trim() === "") {
|
|
errors.push("name is required and must be a non-empty string");
|
|
}
|
|
|
|
if (!m.version || typeof m.version !== "string" || m.version.trim() === "") {
|
|
errors.push("version is required and must be a non-empty string");
|
|
} else if (!/^\d+\.\d+\.\d+$/.test(m.version)) {
|
|
errors.push("version must be a valid semver string (e.g., 1.0.0)");
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
/**
|
|
* Type-safe helper for defining a Fusion plugin.
|
|
*
|
|
* Provides autocompletion and compile-time validation for plugin definitions.
|
|
* This is an identity function - it returns the input unchanged.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* export default definePlugin({
|
|
* manifest: {
|
|
* id: "my-plugin",
|
|
* name: "My Plugin",
|
|
* version: "1.0.0",
|
|
* description: "Does something cool",
|
|
* },
|
|
* hooks: {
|
|
* onLoad: async (ctx) => {
|
|
* ctx.logger.info("Plugin loaded!");
|
|
* },
|
|
* onTaskCompleted: async (task, ctx) => {
|
|
* ctx.logger.info(`Task ${task.id} completed!`);
|
|
* },
|
|
* },
|
|
* tools: [
|
|
* {
|
|
* name: "my_tool",
|
|
* description: "Does something useful",
|
|
* parameters: { type: "object", properties: { input: { type: "string" } } },
|
|
* execute: async (params, ctx) => ({
|
|
* content: [{ type: "text", text: `Processed: ${params.input}` }],
|
|
* }),
|
|
* },
|
|
* ],
|
|
* });
|
|
* ```
|
|
*/
|
|
export function definePlugin(plugin: FusionPlugin): FusionPlugin {
|
|
return plugin;
|
|
}
|