with one click
build-tools-tests
// Build integration tests for MCP tool collections. Reads .discover.json and creates test setup, builders, helpers, and test files per collection. Use after running '/build-tools'.
// Build integration tests for MCP tool collections. Reads .discover.json and creates test setup, builders, helpers, and test files per collection. Use after running '/build-tools'.
Load MCP development patterns and best practices for building tools with the @umbraco-cms/mcp-server-sdk. Use when starting tool development or needing pattern reference.
Guide for configuring and running Umbraco MCP servers via the CLI. Use when the user wants to set up an MCP server for Claude Code, configure auth, filtering, dry-run, readonly mode, or use introspection commands to understand available tools.
Add or update an LLM eval test for a specific tool or collection. Creates a new eval scenario or updates an existing one to cover new tools. Use when adding eval coverage for new or modified tools.
Add an integration test for a specific tool in an existing collection. Creates a test file following the collection's established patterns. Use when adding tests for new or modified tools, or re-creating deleted tests.
Add a new tool to an existing MCP collection. Creates the tool file, updates the collection index, optionally adds integration tests and eval tests. Use when adding new API endpoints to collections already created by '/build-tools'.
Build LLM eval tests for MCP tool collections. Reads .discover.json and creates eval setup and scenario test files per collection. Use after running '/build-tools'.
| name | build-tools-tests |
| description | Build integration tests for MCP tool collections. Reads .discover.json and creates test setup, builders, helpers, and test files per collection. Use after running '/build-tools'. |
| user_invocable | true |
Generate integration tests for MCP tool collections created by /build-tools. This skill reads .discover.json and the existing tool files, then builds test infrastructure and test files one collection at a time.
IMPORTANT: This skill ONLY creates files inside __tests__/ directories ā test files, setup, builders, and helpers. Do NOT create or modify ANY other files. This means: no tool files, no collection indexes, no registrations, no API client files (src/umbraco-api/api/), no generated code (src/umbraco-api/api/generated/), no mock handlers (src/mocks/). If existing code doesn't support what you need, work within the constraints ā do not modify it.
Before running, ensure:
/build-tools (tool collections exist in src/umbraco-api/tools/)npm run compileumbraco-back-office-mcp and Client Secret 1234567890".discover.json/build-tools-tests form)This skill orchestrates the following agents ā use them for the relevant steps:
| Agent | When to use |
|---|---|
test-builder-helper-creator | Creating builders and helpers (Steps 4-5) |
integration-test-creator | Creating test files (Step 7) |
integration-test-validator | Validating test quality (Step 8) |
ONE FILE AT A TIME. This applies to ALL files ā builders, helpers, builder tests, and tool tests. After creating any file:
npm run compilenpm test -- path/to/file.test.tsRUN COMMANDS SEPARATELY. Always run compile and test as separate Bash calls. Never chain them with &&.
NEVER:
&& (run each command separately)ONE TEST FILE PER TOOL. Every tool gets its own test file. Never combine tests for multiple tools into one file.
SNAPSHOT TESTING PREFERRED. Use createSnapshotResult from @umbraco-cms/mcp-server-sdk/testing with toMatchSnapshot() for success responses. Only use assertion testing (expect(x).toBe(y)) for error cases where isError is checked.
REAL API ā NO MOCKING. These are integration tests that run against a real Umbraco instance. Do NOT set USE_MOCK_API. Do NOT create, modify, or reference anything in src/mocks/. Do NOT import server from mocks. Do NOT add MSW handlers. Do NOT use any mocking framework. The tests call tool handlers directly and those handlers call the real API. If a test fails, the fix is in the test or the tool ā never add mock infrastructure.
If testing hits roadblocks ā builders can't create data, APIs reject requests due to missing configuration, or features aren't available ā you are able to manipulate the Umbraco instance to your needs. You can add connection strings, change settings, install packages, or even write C# code in demo-site/. Read instance-management.md in this skill directory for the full process and concrete examples.
Process one collection at a time. Complete each collection fully before starting the next.
Read .discover.json from the project root:
{
"apiName": "Umbraco Forms Management API",
"swaggerUrl": "https://localhost:44324/umbraco/swagger/forms-management/swagger.json",
"baseUrl": "https://localhost:44324",
"collections": ["form", "form-template", "field-type", "folder"]
}
If an argument was provided, filter to only that collection. If .discover.json doesn't exist, tell the user to run npx @umbraco-cms/create-umbraco-mcp-server discover first.
For each collection, read:
src/umbraco-api/tools/{collection}/index.ts ā to get the list of toolssrc/umbraco-api/api/generated/ ā to identify the API client function and Zod schemasIf src/umbraco-api/tools/{collection}/index.ts doesn't exist, skip ā tell the user to run /build-tools first.
Skip if src/umbraco-api/tools/{collection}/__tests__/setup.ts already exists ā tests have already been created for this collection.
Create src/umbraco-api/tools/{collection}/__tests__/setup.ts:
import {
setupTestEnvironment,
createMockRequestHandlerExtra,
createSnapshotResult,
} from "@umbraco-cms/mcp-server-sdk/testing";
import { configureApiClient, initializeUmbracoFetch } from "@umbraco-cms/mcp-server-sdk";
import { getYourAPI } from "../../../../api/generated/yourApi.js";
import { EntityBuilder } from "./helpers/{entity}-builder.js";
import { EntityTestHelper } from "./helpers/{entity}-test-helper.js";
// Initialize fetch with credentials ā required for integration tests hitting the real API
initializeUmbracoFetch({
baseUrl: process.env.UMBRACO_BASE_URL!,
clientId: process.env.UMBRACO_CLIENT_ID!,
clientSecret: process.env.UMBRACO_CLIENT_SECRET!,
});
configureApiClient(() => getYourAPI());
export {
setupTestEnvironment,
createMockRequestHandlerExtra,
createSnapshotResult,
EntityBuilder,
EntityTestHelper,
};
Key rules:
src/umbraco-api/api/generated/initializeUmbracoFetch MUST be called before configureApiClient ā it sets up the authenticated fetch layerUSE_MOCK_API ā these tests run against the real Umbraco instancecreateSnapshotResult for snapshot testingCompile after creating: npm run compile. Fix errors before continuing.
Read-only collections: If the collection has no create or delete operations (e.g. analytics ā only GET/query tools), skip steps 4-6 (builder, helper, builder tests). These steps create test data lifecycle management which isn't needed for read-only collections. Proceed directly to step 7 (integration tests).
Use the test-builder-helper-creator agent.
Create src/umbraco-api/tools/{collection}/__tests__/helpers/{entity}-builder.ts:
import { getYourAPI } from "../../../../api/generated/yourApi.js";
import { CAPTURE_RAW_HTTP_RESPONSE } from "@umbraco-cms/mcp-server-sdk";
const TEST_ENTITY_NAME = "_Test Entity";
interface EntityModel {
name: string;
// ... fields matching the POST body schema from the generated *.zod.ts
}
export class EntityBuilder {
private model: EntityModel = {
name: TEST_ENTITY_NAME,
};
private createdId?: string;
withName(name: string): this {
this.model.name = name;
return this;
}
build(): EntityModel {
return { ...this.model };
}
async create(): Promise<this> {
const client = getYourAPI();
// Call the API client's POST method directly (NOT the tool handler)
const response: any = await client.postEntity(
this.model as any,
CAPTURE_RAW_HTTP_RESPONSE,
);
if (response.status !== 201) {
const errorBody = await response.data?.detail || `HTTP ${response.status}`;
throw new Error(`Failed to create entity: ${errorBody}`);
}
// Extract ID from Location header (Umbraco convention: /api/v1/entity/{id})
const location = response.headers?.get?.("location") || response.headers?.location;
this.createdId = location?.split("/").pop();
return this;
}
async delete(): Promise<void> {
if (!this.createdId) return;
const client = getYourAPI();
try {
await client.deleteEntityById(this.createdId, CAPTURE_RAW_HTTP_RESPONSE);
} catch {
// Ignore delete failures in cleanup
}
this.createdId = undefined;
}
getId(): string {
if (!this.createdId) {
throw new Error("Entity not created yet. Call create() first.");
}
return this.createdId;
}
}
Key rules:
withX methods return thisbuild() returns the model, create() calls the APITEST_ prefix for constantsCompile after creating: npm run compile. Fix errors before continuing.
Use the test-builder-helper-creator agent.
Create src/umbraco-api/tools/{collection}/__tests__/helpers/{entity}-test-helper.ts:
export class EntityTestHelper {
static async findByName(name: string): Promise<any | undefined> {
// Use list tool or API client to find entity
}
static async cleanup(namePrefix: string): Promise<void> {
// List entities and delete those matching prefix
}
static normalizeIds(data: any): any {
// Replace UUIDs with zeroed placeholder for snapshots
if (Array.isArray(data)) {
return data.map(item => this.normalizeIds(item));
}
if (data && typeof data === "object") {
const normalized = { ...data };
if (normalized.id) {
normalized.id = "00000000-0000-0000-0000-000000000000";
}
for (const key of Object.keys(normalized)) {
if (typeof normalized[key] === "object") {
normalized[key] = this.normalizeIds(normalized[key]);
}
}
return normalized;
}
return data;
}
}
Compile after creating: npm run compile. Fix errors before continuing.
Create src/umbraco-api/tools/{collection}/__tests__/helpers/{entity}-builder.test.ts:
import {
setupTestEnvironment,
createMockRequestHandlerExtra,
EntityBuilder,
EntityTestHelper,
} from "../setup.js";
const TEST_NAME = "_Test Builder Entity";
describe("EntityBuilder", () => {
setupTestEnvironment();
let builder: EntityBuilder;
afterEach(async () => {
// Always clean up created entities to prevent conflicts with other test files
if (builder) await builder.delete();
await EntityTestHelper.cleanup(TEST_NAME);
});
it("should create entity with builder", async () => {
const builder = await new EntityBuilder()
.withName(TEST_NAME)
.create();
expect(builder.getId()).toBeDefined();
const found = await EntityTestHelper.findByName(TEST_NAME);
expect(found).toBeDefined();
expect(found?.name).toBe(TEST_NAME);
});
});
After creating:
npm run compilenpm test -- __tests__/{collection}/{entity}-builder.test.tsUse the integration-test-creator agent.
Create one test file per tool. Each tool gets its own .test.ts file. Create and run each sequentially.
import {
setupTestEnvironment,
createMockRequestHandlerExtra,
createSnapshotResult,
EntityBuilder,
} from "./setup.js";
import getEntityTool from "../get/get-entity.js";
describe("get-entity", () => {
setupTestEnvironment();
let builder: EntityBuilder;
afterEach(async () => {
// Clean up test data after each test to prevent conflicts
if (builder) await builder.delete();
});
it("should return entity by ID", async () => {
const context = createMockRequestHandlerExtra();
builder = await new EntityBuilder()
.withName("_Test Get Entity")
.create();
const result = await getEntityTool.handler(
{ id: builder.getId() },
context
);
expect(
createSnapshotResult(result, builder.getId())
).toMatchSnapshot();
});
it("should return error for non-existent ID", async () => {
const context = createMockRequestHandlerExtra();
const result = await getEntityTool.handler(
{ id: "00000000-0000-0000-0000-000000000000" },
context
);
expect(result.isError).toBe(true);
});
});
Key rules:
createSnapshotResult(result, id) for success responses ā it normalizes IDs, dates, and dynamic valuescreateSnapshotResult so it gets normalizedtoMatchSnapshot() ā not toMatchInlineSnapshot()expect(x).toBe(y)) for error casesNEVER access result properties directly. The following patterns are WRONG and will fail:
// WRONG ā result.content may be undefined
const data = JSON.parse(result.content[0].text);
// WRONG ā result structure varies by output mode
expect(result.content).toContain("something");
Always use the snapshot helper:
// CORRECT ā handles all output modes
expect(createSnapshotResult(result, builder.getId())).toMatchSnapshot();
Paginated tools (those with skip/take in their input schema) use cursor-based pagination at runtime via withCursorPagination. Tests for these tools must use the cursor wrapper:
import { withCursorPagination } from "@umbraco-cms/mcp-server-sdk";
import { type CursorPaginatedResult } from "@umbraco-cms/mcp-server-sdk/testing";
import listEntitiesTool from "../get/list-entities.js";
it("should list entities", async () => {
const cursorTool = withCursorPagination(listEntitiesTool);
const result = await cursorTool.handler({}, createMockRequestHandlerExtra());
// Cast to CursorPaginatedResult for nextCursor access
const data = validateToolResponse(cursorTool, result) as CursorPaginatedResult;
expect(data.items.length).toBeGreaterThan(0);
});
it("should paginate with cursor", async () => {
// Use a small pageSize to force multiple pages
const cursorTool = withCursorPagination({ ...listEntitiesTool, pageSize: 1 });
const page1 = await cursorTool.handler({}, createMockRequestHandlerExtra());
const data1 = validateToolResponse(cursorTool, page1) as CursorPaginatedResult;
expect(data1.nextCursor).toBeDefined();
const page2 = await cursorTool.handler(
{ cursor: data1.nextCursor },
createMockRequestHandlerExtra()
);
const data2 = validateToolResponse(cursorTool, page2) as CursorPaginatedResult;
expect(data2.items[0]).not.toEqual(data1.items[0]);
});
Key rules:
skip or take to handlers ā use withCursorPagination() and the cursor param{} for first page (no cursor = first page with default page size){ ...tool, pageSize: N } to override page size for pagination testsvalidateToolResponse results to CursorPaginatedResult from @umbraco-cms/mcp-server-sdk/testingencodeCursor({ s: 10000, t: 10 }) as cursor value| Tool file | Test file |
|---|---|
get/get-{entity}.ts | __tests__/get-{entity}.test.ts |
get/list-{entities}.ts | __tests__/list-{entities}.test.ts |
post/create-{entity}.ts | __tests__/create-{entity}.test.ts |
put/update-{entity}.ts | __tests__/update-{entity}.test.ts |
delete/delete-{entity}.ts | __tests__/delete-{entity}.test.ts |
isError)For each test file:
npm run compilenpm test -- __tests__/{collection}/{test-file}.test.tsintegration-test-validatorAfter all test files pass for a collection, run the integration-test-validator agent. The agent will check:
setupTestEnvironment() used in every describe blockconfigureApiClient() called in setup.tscreateMockRequestHandlerExtra() used for all handler callscreateSnapshotResult + toMatchSnapshot)__tests__/helpers/ directoryTEST_ prefixUSE_MOCK_API is NOT set ā tests hit the real API)Flag any issues but continue to the next collection.
Repeat steps 3-8 for the next collection in .discover.json.
After all collections have tests:
npm run compile # Full type check
npm test # All integration tests
Then run /count-mcp-tools to confirm all collections have tests. All collections should show "yes" in the Tests column. If any show "no", report which collections are missing integration tests.
Report what was generated:
After running, each collection should have:
src/umbraco-api/tools/{collection}/
āāā __tests__/
āāā setup.ts # Shared setup, re-exports
āāā helpers/
ā āāā {entity}-builder.ts # Fluent builder
ā āāā {entity}-builder.test.ts # Tests the builder itself
ā āāā {entity}-test-helper.ts # Find, cleanup, normalizeIds
āāā get-{entity}.test.ts # One file per tool
āāā list-{entities}.test.ts
āāā create-{entity}.test.ts
āāā update-{entity}.test.ts
āāā delete-{entity}.test.ts