Code generation for file-based MCP application development.
Discovers tool, workflow, and UI definitions from conventional file locations and generates a typed manifest for use with createFileBasedApp. Includes a Vite plugin for seamless integration and CLI tools for standalone usage.
- Background
- Features
- Compatibility
- Install
- Usage
- Configuration
- File Structure
- CLI Commands
- API Versioning
- Middleware & Event Handlers
- API
- Examples
- Contributing
- License
Building MCP applications typically requires manually wiring together tool definitions, UI resources, and workflows. This package automates that process by scanning conventional directories and generating a typed manifest that can be imported directly into your app. The result is a file-based development experience where adding a new tool is as simple as creating a new file.
- Vite Plugin: Integrate codegen into your Vite build pipeline with HMR support
- CLI Tools: Generate manifests and start servers without Vite
- File-Based Discovery: Scan
tools/,workflows/,ui/, and more for definitions - Typed Manifests: Generate TypeScript with full type inference
- API Versioning: Support multiple API versions from a single codebase
- Hot Reload: Watch mode with automatic regeneration and tool hot-swapping
- Middleware Support: File-based middleware with configurable ordering
- Event Handlers: File-based event handler discovery
- Node.js:
>= 20 - Vite:
^5.0.0 || ^6.0.0 || ^7.0.0(optional peer dependency) - @mcp-apps-kit/core:
>= 0.5.0(peer dependency)
npm install @mcp-apps-kit/codegen// vite.config.ts
import { defineConfig } from "vite";
import { mcpAppsPlugin } from "@mcp-apps-kit/codegen";
export default defineConfig({
plugins: [
mcpAppsPlugin({
configPath: "./mcp.config.ts",
outDir: "__generated__",
watch: true,
}),
],
});// mcp.config.ts
import { defineConfig } from "@mcp-apps-kit/codegen";
export default defineConfig({
name: "my-app",
version: "1.0.0",
config: {
protocol: "mcp",
cors: { origin: true },
},
});Generate the manifest without Vite:
npx @mcp-apps-kit/codegen
# or
mcp-codegenGenerate and start the server:
mcp-serve --port 3000 --watch// server.ts
import { createFileBasedApp } from "@mcp-apps-kit/core";
import config from "./mcp.config";
import { tools, workflows } from "./__generated__/app-manifest";
const app = createFileBasedApp({
...config,
tools,
});
await app.start({ port: 3000 });Create mcp.config.ts in your project root:
import { defineConfig } from "@mcp-apps-kit/codegen";
export default defineConfig({
// Required
name: "my-app",
version: "1.0.0",
// Optional: Override default directories
directories: {
tools: "tools", // Default: "tools"
workflows: "workflows", // Default: "workflows"
ui: "ui", // Default: "ui"
uiWidgets: "ui/widgets", // Default: undefined (disabled)
middleware: "middleware", // Default: undefined (disabled)
handlers: "handlers", // Default: undefined (disabled)
},
// Optional: Global config passed to createFileBasedApp
config: {
protocol: "mcp",
cors: { origin: true },
debug: { level: "info" },
},
// Optional: Plugins
plugins: [],
// Optional: App icon
icon: "https://example.com/icon.png",
});Place your definitions in conventional directories:
my-app/
├── mcp.config.ts
├── tools/
│ ├── greet.ts # → tool: greet
│ ├── get-weather.ts # → tool: get_weather
│ └── users/
│ └── create.ts # → tool: users_create
├── workflows/
│ └── onboarding.ts # → workflow: onboarding
├── ui/
│ ├── widgets/ # Convention-based UI widgets
│ │ └── get-weather.tsx # Auto-binds to get_weather tool
│ └── dashboard.html # Static UI resource
├── middleware/
│ └── logging.ts # → middleware
├── handlers/
│ └── app-lifecycle.ts # → event handler
└── __generated__/
└── app-manifest.ts # Generated manifest
Files prefixed with _ (e.g., _shared.ts) are excluded from discovery.
Use the fluent tool builder API:
// tools/get-current-weather.ts
import { tool } from "@mcp-apps-kit/core";
import { z } from "zod";
export default tool
.describe("Get current weather conditions for a location")
.input({
location: z.string().describe("City name or address"),
})
.output({
temperature: z.number(),
humidity: z.number(),
})
.readOnly()
.handle(async ({ location }) => {
return { temperature: 22, humidity: 65 };
});
// Note: .build() is optional - codegen auto-buildsOr use defineTool for the object syntax:
// tools/greet.ts
import { defineTool } from "@mcp-apps-kit/core";
import { z } from "zod";
export default defineTool({
description: "Greet a user",
input: z.object({ name: z.string() }),
output: z.object({ message: z.string() }),
handler: async ({ name }) => ({
message: `Hello, ${name}!`,
}),
});When uiWidgets is configured, widget files are automatically bound to tools by matching filenames. Export a ui constant with widget metadata:
// ui/widgets/get-current-weather.tsx
import { useToolResult, useHostContext } from "@mcp-apps-kit/ui-react";
import type { WidgetMetadata } from "@mcp-apps-kit/core";
// Widget metadata - html path is auto-inferred from filename
export const ui: WidgetMetadata = {
name: "Current Weather Widget",
description: "Displays weather conditions",
prefersBorder: true,
};
export default function CurrentWeatherWidget() {
const result = useToolResult();
const context = useHostContext();
// ... render widget
}Tools can also export a ui named export for colocated UI:
// tools/search.ts
import { defineTool, defineUI } from "@mcp-apps-kit/core";
import { z } from "zod";
export const ui = defineUI({
name: "Search Results",
html: "./dist/search-widget.html",
});
export default defineTool({
description: "Search items",
input: z.object({ query: z.string() }),
handler: async ({ query }) => ({ results: [] }),
ui,
});Generate the manifest from mcp.config.ts:
mcp-codegenGenerate manifest and start the MCP server:
mcp-serve [options]
Options:
-p, --port <number> Port to listen on (default: 3000 or PORT env)
-w, --watch Enable hot reload on file changes
-c, --config <path> Path to config file (default: ./mcp.config.ts)Support multiple API versions with versioned configuration:
// mcp.config.ts
import { defineConfig } from "@mcp-apps-kit/codegen";
export default defineConfig({
name: "my-api",
config: {
cors: { origin: true },
},
versions: {
v1: {
version: "1.0.0",
// Uses default: versions/v1/tools, versions/v1/workflows, etc.
},
v2: {
version: "2.0.0",
directories: {
root: "versions/v2",
},
config: {
debug: { level: "debug" },
},
},
},
});Directory structure for versioned apps:
my-api/
├── mcp.config.ts
├── versions/
│ ├── v1/
│ │ ├── tools/
│ │ └── workflows/
│ └── v2/
│ ├── tools/
│ └── workflows/
└── __generated__/
├── v1-manifest.ts
├── v2-manifest.ts
└── versions-manifest.ts
Each version is exposed at its dedicated route:
- v1:
http://localhost:3000/v1/mcp - v2:
http://localhost:3000/v2/mcp
Use defineMiddleware for simple middleware:
// middleware/logging.ts
import { defineMiddleware } from "@mcp-apps-kit/codegen";
export default defineMiddleware({
before: async (context) => {
context.state.set("startTime", Date.now());
console.log(`Tool called: ${context.toolName}`);
},
after: async (context) => {
const startTime = context.state.get("startTime") as number;
const duration = Date.now() - startTime;
console.log(`Tool completed: ${context.toolName} (${duration}ms)`);
},
});Use defineOrderedMiddleware when you need explicit ordering:
// middleware/auth.ts
import { defineOrderedMiddleware } from "@mcp-apps-kit/codegen";
export default defineOrderedMiddleware({
order: 10, // Lower numbers run first (default: 100)
before: async (context) => {
if (!context.metadata.subject) {
throw new Error("Authentication required");
}
},
});// handlers/app-lifecycle.ts
import { defineHandler, Events } from "@mcp-apps-kit/codegen";
export default defineHandler({
event: Events.APP_START,
handler: async (payload) => {
console.log(`Server started on port ${payload.port}`);
},
});// handlers/tool-metrics.ts
import { defineHandler, Events } from "@mcp-apps-kit/codegen";
export default defineHandler({
event: Events.TOOL_SUCCESS,
handler: async (payload) => {
analytics.track("tool_success", {
tool: payload.toolName,
duration: payload.duration,
});
},
});Available events: APP_INIT, APP_START, APP_SHUTDOWN, TOOL_CALLED, TOOL_SUCCESS, TOOL_ERROR, ERROR
mcpAppsPlugin(options?)- Vite plugin for manifest generationoptions.configPath- Path to config file (default:"./mcp.config.ts")options.outDir- Output directory (default:"__generated__")options.watch- Enable file watching (default:true)
defineConfig(config)- Type-safe configuration helper
generateManifest(options)- Generate manifest for single-version configgenerateVersionedManifests(config, projectRoot, outDir, logger)- Generate manifests for versioned configrunCodegen(options?)- Run the full codegen pipeline
defineApp(options)- Create and optionally auto-start a file-based app
defineMiddleware(definition)- Define middleware with before/after hooks (re-exported from core)defineOrderedMiddleware(definition)- Define middleware with explicitorderproperty for sortingdefineHandler(definition)- Define an event handler with{ event, handler }objectEvents- Event name constants (APP_START,TOOL_CALLED, etc.)
pathToIdentifier(path)- Convert file path to snake_case identifiershouldSkipFile(filename)- Check if file should be excludedfindNameCollisions(files)- Detect duplicate identifiers
FileBasedConfig- Single-version configurationVersionedFileBasedConfig- Multi-version configurationDirectoriesConfig- Directory path overridesMcpAppsPluginOptions- Vite plugin optionsDiscoveredFile- Discovered file metadataManifestResult- Generation result with code and diagnostics
../../examples/weather-app- Full example demonstrating file-based development with tools, workflows, middleware, handlers, and UI widgets
See ../../CONTRIBUTING.md for development setup and guidelines. Issues and pull requests are welcome.
MIT