Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6f01c6b
docs: PRD + ADRs for flow skills and MCP servers
claude Jun 30, 2026
1e021b6
feat: upload SKILL.md and attach skills to conversational steps (v1.5…
claude Jun 30, 2026
d16d085
feat: MCP server registry + admin management (Phase 2a, v1.53.0)
claude Jun 30, 2026
6de3632
feat: MCP flow consumption — mcp node + conversational tool-loop (Pha…
claude Jun 30, 2026
87a7ed1
Merge branch 'main' into claude/skill-upload-mcp-servers-5vvoxr
johntooth Jun 30, 2026
94df0b2
fix: resolve merge conflicts and version collision with main
claude Jun 30, 2026
0dee5cb
test: raise skill/mcp use-case coverage during QA
claude Jun 30, 2026
75d1254
Merge remote-tracking branch 'origin/main' into pr-132-mcp-servers
Jul 1, 2026
f90e978
feat: separate MCP/Skills flags, per-server Context/Actions split, UI…
claude Jul 1, 2026
1a270c9
fix: hide MCP/Skills UI surfaces when their feature flags are off
claude Jul 2, 2026
05748b1
feat: Phase B — agentic write-MCP node, arg editing, idempotency, n8n…
claude Jul 2, 2026
f7c778a
Merge remote-tracking branch 'origin/main' into claude/ui-mcp-skills-…
claude Jul 2, 2026
059af8a
fix: gate the n8n Flow Settings page on the auto_node feature flag
claude Jul 2, 2026
b222882
Merge remote-tracking branch 'origin/main' into claude/ui-mcp-skills-…
claude Jul 5, 2026
8b5422a
docs: reorganise implemented docs into alpha release lines; bump to 2…
claude Jul 5, 2026
aa0ced2
fix(db): re-sequence MCP/Skills migration after main's 0028 (fixes E2…
claude Jul 5, 2026
efbae2c
docs: update stale implemented/ references to the alpha release-line …
claude Jul 5, 2026
3083872
docs: add v1.58.6 doc-generation step-flow fix docs under alpha-1
claude Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion .claude/commands/bugfix.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ Write at least one Playwright e2e test that exercises the fixed behaviour throug

### Step 6 — On completion

- Move bug-fix doc: `to-be-implemented/<name>.md` → `implemented/v[version]/<name>.md`
- Move bug-fix doc: `to-be-implemented/<name>.md` → `implemented/alpha-<major>/v[version]/<name>.md`
(`alpha-<major>` is the current release line — `alpha-2` for `2.x.x`; see `docs/guides/versioning.md`)
- Write an implementation summary: root cause, fix applied, regression test added, e2e test added
- Apply a PATCH version bump
- Update `VERSION` and root `package.json` `version`
Expand Down
5 changes: 3 additions & 2 deletions .claude/commands/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ Once all sub-components pass validation, write at least one Playwright e2e test

### Step 4 — On completion

- Move phase doc: `to-be-implemented/<name>.md` → `implemented/v[version]/<name>.md`
- Write an implementation summary in `implemented/v[version]/` covering:
- Move phase doc: `to-be-implemented/<name>.md` → `implemented/alpha-<major>/v[version]/<name>.md`
where `alpha-<major>` is the current release line — `alpha-2` for `2.x.x`, `alpha-1` for `1.x.x` (see `docs/guides/versioning.md`)
- Write an implementation summary in `implemented/alpha-<major>/v[version]/` covering:
what was built, files created/modified, migrations run, known limitations, e2e tests added
- Update `VERSION` file and root `package.json` `version` (they must match)
- Run `./validate.sh` one final time — fix all failures before declaring done
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/enhance.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Ask all of these via `AskUserQuestion` before proceeding:
- Cover the primary user-facing behaviour introduced or modified by this enhancement
- The test must pass against the updated code before moving on
5. On completion:
- Move phase doc to `implemented/v[version]/`
- Move phase doc to `implemented/alpha-<major>/v[version]/` (current release line — `alpha-2` for `2.x.x`; see `docs/guides/versioning.md`)
- Write implementation summary (include which e2e test covers the change)
- Apply the version bump
- Run `./validate.sh`
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ the code, validate, document, version, ship.** A few things to know going in:
implementation, in-memory fakes for ports (never mock what you own), and
run `./validate.sh` after every sub-component — not just at the end.
- Every code-writing skill finishes by moving its phase doc into
`docs/development/implemented/v[version]/`, writing an implementation
summary, and bumping `VERSION` / root `package.json` (they must match).
`docs/development/implemented/alpha-<major>/v[version]/`, writing an
implementation summary, and bumping `VERSION` / root `package.json` (they
must match).
- If you're not using a skill — a tiny fix, a doc typo — that's fine, but
still follow the same discipline by hand: test first, `./validate.sh`
before you're done, version bump if the change touches shipped behaviour.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.0
2.2.0
2 changes: 1 addition & 1 deletion apps/web/e2e/enhance-approval-context.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "@playwright/test";

// E2E for the approval-screen context & decision UX enhancement.
// (docs/development/implemented/v1.47.5/approval-context-ux.phase.md)
// (docs/development/implemented/alpha-1/v1.47.5/approval-context-ux.phase.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running stack — excluded
// from the vitest unit run. Assumes the seeded pending approval from
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/fix-confidence-threshold-scale.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
// E2E for bug #1: a node whose advanceConfidenceThreshold was authored as a
// fraction (0.7) instead of a percentage (70) must NOT auto-advance on its own
// opening message before the user has answered anything.
// (docs/development/implemented/v1.49.0/fix-flow-authored-data-trust.md)
// (docs/development/implemented/alpha-1/v1.49.0/fix-flow-authored-data-trust.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running stack with a real
// AI key. On the unfixed code, `5 >= 0.7` was always true, so every step
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
// E2E for bug #3: producing a document for a flow whose reference documents are
// very large must no longer overflow the model context window. The context-doc
// section is budget-capped and generation runs in field batches.
// (docs/development/implemented/v1.49.0/fix-flow-authored-data-trust.md)
// (docs/development/implemented/alpha-1/v1.49.0/fix-flow-authored-data-trust.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running stack with a real
// AI key. On the unfixed code, document generation dumped the full extracted
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/fix-fork-advance-threshold.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect, test } from "@playwright/test";
// threshold. The regression gated the branch-choice call on a hardcoded 90, so a
// fork at threshold 70 reported "complete" at confidence 80 yet never advanced —
// the session silently stalled on the fork, turn after turn, with no error.
// (docs/development/implemented/v1.54.0/codebase-bug-fixes.phase.md)
// (docs/development/implemented/alpha-1/v1.54.0/codebase-bug-fixes.phase.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running stack in AI-mock
// mode — excluded from the vitest unit run. The pure gating predicate is unit
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/fix-session-upload-not-reaching-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test";

// E2E for the bug fix: a deliberately-attached session upload must reach the AI
// even when the user's message is only loosely related to the document body.
// (docs/development/implemented/v1.47.1/fix-session-upload-not-reaching-ai.md)
// (docs/development/implemented/alpha-1/v1.47.1/fix-session-upload-not-reaching-ai.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running stack — it is
// excluded from the vitest unit run and requires real embeddings + an AI key,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/fix-sticky-link-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect, test } from "@playwright/test";
// instead of feeling sticky. Before the fix there was no loading.tsx boundary and
// no navigation indicator, so the previous page stayed fully on screen with zero
// feedback while the destination resolved its session + tRPC prefetch.
// (docs/development/implemented/v1.48.4/fix-sticky-link-navigation.md)
// (docs/development/implemented/alpha-1/v1.48.4/fix-sticky-link-navigation.md)
//
// Driven by the /e2e (Playwright MCP) skill against a running, signed-in stack.
// Assumes a seeded user session lands on /chats.
Expand Down
49 changes: 49 additions & 0 deletions apps/web/e2e/phase-flow-skills.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from "@playwright/test";

// E2E for Flow Skills — uploading a SKILL.md to the library and attaching it to a
// conversational step (PRD: flow-skills-and-mcp, ADR-031).
//
// Driven by the /e2e (Playwright MCP) skill against a running stack — it is
// excluded from the vitest unit run. Assumes an authenticated admin storageState
// (the same global setup the other admin specs rely on).

const SKILL_MD = `---
name: E2E Contract Reviewer
description: Flags unusual contract clauses
---

# Contract review

Read the contract and flag unusual indemnity clauses.`;

test.describe("flow skills", () => {
test("an admin can upload a SKILL.md and see it in the library", async ({ page }) => {
await page.goto("/admin/skills");

await page.getByLabel("SKILL.md").fill(SKILL_MD);
await page.getByRole("button", { name: /upload skill/i }).click();

// The parsed name appears in the library table.
await expect(page.getByText("E2E Contract Reviewer")).toBeVisible();
await expect(page.getByText("Flags unusual contract clauses")).toBeVisible();
});

test("an invalid SKILL.md surfaces a validation error and stores nothing", async ({ page }) => {
await page.goto("/admin/skills");

// Frontmatter with no name and no heading cannot be parsed into a skill.
await page.getByLabel("SKILL.md").fill("---\ndescription: no name\n---\n");
await page.getByRole("button", { name: /upload skill/i }).click();

await expect(page.getByText(/must declare a name/i)).toBeVisible();
});

test("an uploaded skill can be archived from the library", async ({ page }) => {
await page.goto("/admin/skills");

const row = page.getByRole("row", { name: /E2E Contract Reviewer/i }).first();
await row.getByRole("button", { name: /archive/i }).click();

await expect(row.getByRole("button", { name: /restore/i })).toBeVisible();
});
});
32 changes: 32 additions & 0 deletions apps/web/e2e/phase-mcp-flow-consumption.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, test } from "@playwright/test";

// E2E for MCP flow consumption (Phase 2b, ADR-032): adding a deterministic MCP
// Tool step to a flow, and allowing MCP tools on a conversational step.
//
// Driven by the /e2e (Playwright MCP) skill against a running stack — excluded
// from the vitest unit run. Assumes an authenticated admin storageState and at
// least one flow plus one registered MCP server. Selectors mirror the canvas
// node-type picker and the step config modal.

test.describe("mcp flow consumption", () => {
test("an author can add an MCP Tool step from the node picker", async ({ page }) => {
await page.goto("/admin/flows");
await page.getByRole("link", { name: /flow/i }).first().click();

await page.getByRole("button", { name: /add step/i }).first().click();
await page.getByText("MCP Tool", { exact: true }).click();

// The MCP step config exposes a server selector.
await expect(page.getByLabel("MCP server")).toBeVisible();
});

test("a conversational step exposes an allowed MCP tools picker", async ({ page }) => {
await page.goto("/admin/flows");
await page.getByRole("link", { name: /flow/i }).first().click();

// Open the first conversational step's config (double-click its canvas node).
await page.locator(".react-flow__node").first().dblclick();

await expect(page.getByText("MCP tools", { exact: true })).toBeVisible();
});
});
42 changes: 42 additions & 0 deletions apps/web/e2e/phase-mcp-integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "@playwright/test";

// E2E for MCP server registration (Phase 2a of the Flow Skills & MCP PRD,
// ADR-032).
//
// Driven by the /e2e (Playwright MCP) skill against a running stack — it is
// excluded from the vitest unit run. Assumes an authenticated admin storageState.
// Connection Test is not asserted here because it requires a live MCP server.

test.describe("mcp servers", () => {
test("an admin can register a remote MCP server and see it listed", async ({ page }) => {
await page.goto("/admin/mcp-servers");

await page.getByLabel("Label").fill("E2E GitHub");
await page.getByLabel("SSE URL").fill("https://mcp.example.com/sse");
await page.getByLabel(/credential ref/i).fill("MCP_E2E_TOKEN");
await page.getByRole("button", { name: /register server/i }).click();

const row = page.getByRole("row", { name: /E2E GitHub/i }).first();
await expect(row).toBeVisible();
await expect(row.getByText("active")).toBeVisible();
});

test("an invalid URL surfaces a validation error", async ({ page }) => {
await page.goto("/admin/mcp-servers");

await page.getByLabel("Label").fill("Bad Server");
await page.getByLabel("SSE URL").fill("not-a-url");
await page.getByRole("button", { name: /register server/i }).click();

await expect(page.getByText(/valid http\(s\) URL/i)).toBeVisible();
});

test("a registered server can be disabled", async ({ page }) => {
await page.goto("/admin/mcp-servers");

const row = page.getByRole("row", { name: /E2E GitHub/i }).first();
await row.getByRole("button", { name: /^disable$/i }).click();

await expect(row.getByRole("button", { name: /^enable$/i })).toBeVisible();
});
});
45 changes: 44 additions & 1 deletion apps/web/src/app/(admin)/admin/flows/[id]/_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { ConversationalNodeData } from "@/components/canvas/conversational-
import { ConversationalNode } from "@/components/canvas/conversational-node";
import type { AutoNodeData } from "@/components/canvas/auto-node";
import { AutoNode } from "@/components/canvas/auto-node";
import { McpNode, type McpNodeData } from "@/components/canvas/mcp-node";
import type { ScheduledNodeData } from "@/components/canvas/scheduled-node";
import { ScheduledNode } from "@/components/canvas/scheduled-node";
import type { ApprovalNodeData } from "@/components/canvas/approval-node";
Expand All @@ -55,6 +56,7 @@ const NODE_TYPES = {
autoNode: AutoNode,
scheduledNode: ScheduledNode,
approvalNode: ApprovalNode,
mcpNode: McpNode,
};

const DEBOUNCE_MS = 600;
Expand All @@ -63,7 +65,7 @@ interface RawNode {
id: string;
name: string;
colour: string | null;
type?: "conversational" | "auto" | "scheduled" | "approval";
type?: "conversational" | "auto" | "scheduled" | "approval" | "mcp";
positionX: number;
positionY: number;
config: Record<string, unknown>;
Expand Down Expand Up @@ -110,6 +112,17 @@ const toRfNode = (node: RawNode, stepNumber: number | null): Node => {
return { id: node.id, type: "approvalNode", position: { x: node.positionX, y: node.positionY }, data };
}

if (node.type === "mcp") {
const data: McpNodeData = {
name: node.name,
colour: node.colour,
toolName: (node.config.toolName as string | null) ?? null,
stepNumber,
config: node.config,
};
return { id: node.id, type: "mcpNode", position: { x: node.positionX, y: node.positionY }, data };
}

const data: ConversationalNodeData = {
name: node.name,
colour: node.colour,
Expand Down Expand Up @@ -142,6 +155,7 @@ function CanvasInner({ flowId }: { flowId: string }) {
const [rfNodes, setRfNodes] = useState<Node[]>([]);
const [rfEdges, setRfEdges] = useState<Edge[]>([]);
const [contextDocs, setContextDocs] = useState<FlowContextDoc[]>([]);
const [contextMcpServerIds, setContextMcpServerIds] = useState<string[]>([]);
const [flowName, setFlowName] = useState("");
const [flowStatus, setFlowStatus] = useState<"draft" | "published">("draft");
const [flowVisibility, setFlowVisibility] = useState<"private" | "global">("private");
Expand Down Expand Up @@ -171,6 +185,8 @@ function CanvasInner({ flowId }: { flowId: string }) {
const autoNodeEnabled = trpc.featureFlag.isEnabledForMe.useQuery({ key: "auto_node" }).data ?? false;
const scheduledNodeEnabled =
trpc.featureFlag.isEnabledForMe.useQuery({ key: "scheduled_node" }).data ?? false;
const mcpEnabled = trpc.featureFlag.isEnabledForMe.useQuery({ key: "mcp" }).data ?? false;
const skillsEnabled = trpc.featureFlag.isEnabledForMe.useQuery({ key: "skills" }).data ?? false;

const [configOpen, setConfigOpen] = useState(false);
const [typePickerOpen, setTypePickerOpen] = useState(false);
Expand Down Expand Up @@ -217,6 +233,7 @@ function CanvasInner({ flowId }: { flowId: string }) {
setRfNodes(data.nodes.map((n) => toRfNode(n, null)));
setRfEdges(data.edges.map(toRfEdge));
setContextDocs(data.flow.contextDocs);
setContextMcpServerIds(data.flow.contextMcpServerIds ?? []);
setFlowName(data.flow.name);
setFlowStatus(data.flow.status);
setFlowVisibility(data.flow.visibility.kind);
Expand Down Expand Up @@ -373,6 +390,15 @@ function CanvasInner({ flowId }: { flowId: string }) {
notifyOnComplete: values.notifyOnComplete,
};
}
if (values.type === "mcp") {
return {
instruction: values.instruction,
serverId: values.mcpServerId,
allowedToolNames: values.mcpAllowedToolNames,
responseFields: values.responseFields,
requireConfirmation: values.requireConfirmation,
};
}
const hasTemplate = values.outputType === "generate_document" && !!values.documentTemplatePath;
return {
aiInstruction: values.aiInstruction,
Expand All @@ -386,6 +412,8 @@ function CanvasInner({ flowId }: { flowId: string }) {
documentTemplateStructuredContent: hasTemplate ? (existingNodeConfig.documentTemplateStructuredContent ?? null) : null,
allowManualEdit: values.allowManualEdit,
requireConfirmation: values.neverDone ? false : values.requireConfirmation,
skillRefs: values.skillRefs,
allowedMcpToolRefs: values.allowedMcpToolRefs,
notifyOnComplete: values.notifyOnComplete,
};
};
Expand Down Expand Up @@ -619,10 +647,19 @@ function CanvasInner({ flowId }: { flowId: string }) {
documentTemplateContent: (editingConfig.documentTemplateContent as string | null) ?? null,
allowManualEdit: (editingConfig.allowManualEdit as boolean | undefined) ?? true,
requireConfirmation: Boolean(editingConfig.requireConfirmation),
skillRefs: (editingConfig.skillRefs as string[] | undefined) ?? [],
allowedMcpToolRefs:
(editingConfig.allowedMcpToolRefs as NodeConfigValues["allowedMcpToolRefs"] | undefined) ??
[],
instruction: (editingConfig.instruction as string | null) ?? "",
executor: (editingConfig.executor as "n8n" | "mock" | undefined) ?? "n8n",
workflowId: (editingConfig.workflowId as string | null) ?? null,
webhookUrl: (editingConfig.webhookUrl as string | null) ?? "",
mcpServerId: (editingConfig.serverId as string | null) ?? "",
mcpToolName: (editingConfig.toolName as string | null) ?? "",
mcpAllowedToolNames:
(editingConfig.allowedToolNames as string[] | undefined) ??
(editingConfig.toolName ? [editingConfig.toolName as string] : []),
requestFields: readFields(editingConfig.requestFields),
requestFieldValues:
(editingConfig.requestFieldValues as Record<string, FieldValueSource> | undefined) ?? {},
Expand Down Expand Up @@ -885,12 +922,16 @@ function CanvasInner({ flowId }: { flowId: string }) {
flowId={flowId}
docs={contextDocs}
onDocsChange={setContextDocs}
mcpEnabled={mcpEnabled}
contextMcpServerIds={contextMcpServerIds}
onContextMcpServerIdsChange={setContextMcpServerIds}
/>

<NodeTypePickerModal
open={typePickerOpen}
autoNodeEnabled={autoNodeEnabled}
scheduledNodeEnabled={scheduledNodeEnabled}
mcpNodeEnabled={mcpEnabled}
onSelect={handleSelectNodeType}
onClose={() => {
setTypePickerOpen(false);
Expand All @@ -906,6 +947,8 @@ function CanvasInner({ flowId }: { flowId: string }) {
onDelete={editingNodeId ? handleNodeDelete : undefined}
onClose={handleConfigClose}
isSaving={isSavingConfig}
skillsEnabled={skillsEnabled}
mcpEnabled={mcpEnabled}
priorStepFields={priorStepFields}
onUploadTemplate={editingNodeId ? handleUploadTemplate : undefined}
/>
Expand Down
Loading
Loading