MCP server for Umbraco Forms, built on @umbraco-cms/mcp-server-sdk. Exposes forms, data sources, records, workflows, and related Forms management APIs as MCP tools.
- Node.js 22+
- .NET SDK 10.0 and SQL Server reachable at
localhost:1433— only if you're running the bundleddemo-site/(see below). Pointing this server at an existing Umbraco Forms instance instead needs neither.
Already have an Umbraco Forms instance? This server works against any Umbraco Forms install — the
demo-site/in steps 2–4 below is only there to give you something to run against out of the box. If you already have an instance running (locally or remotely), skip straight to step 5 and point.envat it: setUMBRACO_BASE_URLto its URL, andUMBRACO_CLIENT_ID/UMBRACO_CLIENT_SECRETto an API user on that instance (create one via its backoffice, or runnpm run create-api-user <base-url> <admin-email> <admin-password>against it instead of the demo defaults).
npm installdocker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MyStrong!Passw0rd" \
-p 1433:1433 --name forms-mcp-sql -d mcr.microsoft.com/mssql/server:2022-latest
docker exec forms-mcp-sql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'MyStrong!Passw0rd' -C -Q "CREATE DATABASE FormsMcpDb"demo-site/ is a working Umbraco Forms install already checked into this repo.
npm run start:umbracoThe first run performs an unattended install and creates the admin user (admin@admin.com / 1234567890, see demo-site/appsettings.Development.json). Leave it running — subsequent steps talk to it at https://localhost:44390.
In a new terminal, once Umbraco is up:
npm run create-api-userThis provisions an API user (Client ID umbraco-back-office-mcp / Secret 1234567890) via the Management API — see CLAUDE.md for the manual backoffice alternative and version-specific caveats (e.g. the Swagger OAuth redirect path).
cp .env.example .envFill in (or confirm) these values to match the demo site:
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
UMBRACO_CLIENT_SECRET=1234567890
UMBRACO_BASE_URL=https://localhost:44390
NODE_TLS_REJECT_UNAUTHORIZED=0
npm run build
# Test with MCP Inspector
npm run inspectOr open this project directory in Claude Code — .mcp.json registers the server automatically (it runs node --env-file=.env ./dist/index.js, so no secrets leave .env).
├── src/
│ ├── umbraco-api/
│ │ ├── api/
│ │ │ ├── client.ts # API client configuration
│ │ │ └── generated/ # Orval-generated client and Zod schemas
│ │ └── tools/
│ │ └── {collection}/ # e.g. form, data-source, record, workflow-type...
│ │ ├── index.ts # ToolCollectionExport
│ │ ├── get/ post/ put/ delete/
│ │ └── __tests__/
│ ├── config/ # Custom fields, slice/mode registries
│ ├── mocks/ # MSW handlers for unit tests
│ └── index.ts # Server entry point
├── demo-site/ # Local Umbraco Forms instance for dev/testing
├── scripts/
│ ├── create-api-user.mjs # Provisions the MCP API user
│ ├── test-changed.mjs # Runs only tests related to the current diff
│ ├── rerun-failures.mjs # Reruns only the last run's failures
│ └── start-umbraco.sh / .ps1 # Runs demo-site/
├── umbraco/ # Composer snippets to copy into YOUR OWN Umbraco project
│ └── McpOAuthComposer.cs # if self-hosting the MCP server as a Worker
├── tests/evals/ # LLM-based acceptance tests
├── .github/workflows/ # CI (test.yml) and release (release-tag.yml)
└── .env.example
Full tool conventions, registries, and the Umbraco-version check are documented in CLAUDE.md.
- Create a folder under
src/umbraco-api/tools/for your tool collection - Add tool files in the matching subfolder —
get/,post/,put/,delete/ - Add an
index.tsthat exports the collection (ToolCollectionExport) - Register the collection in
src/collections.tsandsrc/index.ts
import { z } from "zod";
import {
withStandardDecorators,
executeGetApiCall,
CAPTURE_RAW_HTTP_RESPONSE,
ToolDefinition,
} from "@umbraco-cms/mcp-server-sdk";
const inputSchema = {
id: z.string().uuid(),
};
const myTool: ToolDefinition<typeof inputSchema> = {
name: "my-tool",
description: "Does something useful",
inputSchema,
slices: ["read"],
annotations: { readOnlyHint: true },
handler: async ({ id }) => {
return executeGetApiCall((client) =>
client.getMyItem(id, CAPTURE_RAW_HTTP_RESPONSE)
);
},
};
export default withStandardDecorators(myTool);Integration tests run against the real Umbraco instance from the Quick Start above (no mocking):
npm test # full integration suite
npm run test:changed # only tests related to files changed vs dev/main
npm run test:rerun-failures # re-run only what failed last time (reads test-failures.log)
npm run test:evals # LLM-based acceptance tests (needs Claude Code subscription or ANTHROPIC_API_KEY)Tests use Jest with the MCP toolkit's testing helpers:
import {
setupTestEnvironment,
createSnapshotResult,
createMockRequestHandlerExtra,
} from "@umbraco-cms/mcp-server-sdk/testing";
describe("my-tool", () => {
setupTestEnvironment();
it("should do something", async () => {
const result = await myTool.handler({ id: "..." }, createMockRequestHandlerExtra());
expect(createSnapshotResult(result)).toMatchSnapshot();
});
});If the Umbraco Forms Management API changes, point orval.config.ts at your instance and regenerate:
npm run generateThis also re-stamps src/config/umbraco-target.generated.ts from your connected instance's actual version — see CLAUDE.md for why there's no spec-based fallback.
.github/workflows/test.ymlspins up SQL Server + a real Umbraco instance and runs the integration suite per tool collection on every push/PR todev/main..github/workflows/release-tag.ymltagsv<version>and creates a GitHub Release wheneverpackage.json's version changes onmain.
See src/worker.ts and CLAUDE.md's "Hosted Worker" section. The umbraco/ folder holds Composer snippets to copy into your own Umbraco project so it can authenticate a Worker-hosted MCP server.
MIT