Skip to content

Commit 44058b0

Browse files
Merge pull request #488 from basementstudio/canary
0.6.3
2 parents 50514fe + e7d293c commit 44058b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2874
-406
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build output
5+
dist/
6+
.xmcp/
7+
worker.js
8+
wrangler.jsonc
9+
10+
# Environment variables (contains secrets)
11+
.dev.vars
12+
13+
# IDE
14+
.idea/
15+
.vscode/
16+
*.swp
17+
*.swo
18+
19+
# OS
20+
.DS_Store
21+
Thumbs.db
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Cloudflare Workers Example
2+
3+
This example demonstrates how to deploy an xmcp MCP server to Cloudflare Workers.
4+
5+
## Getting Started
6+
7+
1. Install dependencies:
8+
9+
```bash
10+
pnpm install
11+
```
12+
13+
2. Build for Cloudflare Workers:
14+
15+
```bash
16+
pnpm build
17+
```
18+
19+
This writes these files into the project root:
20+
21+
- `worker.js` - The bundled Cloudflare Worker
22+
- `wrangler.jsonc` - Wrangler configuration template (only if you don't already have `wrangler.toml/jsonc`)
23+
24+
3. Start development mode (watch + rebuild worker output) and Wrangler:
25+
26+
```bash
27+
pnpm dev
28+
```
29+
30+
This runs `xmcp dev --cf` (rebuilds `worker.js`) and `wrangler dev` together.
31+
32+
4. Test with curl:
33+
34+
```bash
35+
pnpm preview
36+
# or
37+
npx wrangler dev
38+
```
39+
40+
```bash
41+
# Health check
42+
curl http://localhost:8787/health
43+
44+
# List tools
45+
curl -X POST http://localhost:8787/mcp \
46+
-H "Content-Type: application/json" \
47+
-H "Accept: application/json" \
48+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
49+
50+
# Call a tool
51+
curl -X POST http://localhost:8787/mcp \
52+
-H "Content-Type: application/json" \
53+
-H "Accept: application/json" \
54+
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"hello","arguments":{"name":"World"}},"id":2}'
55+
```
56+
57+
5. Deploy to Cloudflare:
58+
59+
```bash
60+
pnpm deploy
61+
# or
62+
npx wrangler deploy
63+
```
64+
65+
## Notes
66+
67+
- The `--cf` flag builds a Cloudflare Workers-native bundle
68+
- All Node.js APIs are bundled into the worker (no external dependencies)
69+
- React component bundles are inlined at compile time
70+
- The worker uses Web APIs only (no Node.js runtime dependencies)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "cloudflare-workers-example",
3+
"version": "0.0.1",
4+
"description": "Example xmcp server deployed to Cloudflare Workers",
5+
"private": true,
6+
"type": "module",
7+
"scripts": {
8+
"build": "xmcp build --cf",
9+
"dev": "concurrently \"xmcp dev --cf\" \"npx wrangler dev\"",
10+
"deploy": "npx wrangler deploy",
11+
"preview": "npx wrangler dev"
12+
},
13+
"dependencies": {
14+
"xmcp": "workspace:*",
15+
"zod": "^3.23.8"
16+
},
17+
"devDependencies": {
18+
"@cloudflare/workers-types": "^4.20241112.0",
19+
"concurrently": "^9.2.0",
20+
"typescript": "^5.7.2",
21+
"wrangler": "^4.62.0"
22+
}
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { z } from "zod";
2+
3+
export const metadata = {
4+
name: "get-weather",
5+
description: "Get the current weather for a location (mock data)",
6+
};
7+
8+
export const schema = {
9+
location: z.string().describe("The city or location to get weather for"),
10+
};
11+
12+
export default async function getWeather({ location }: { location: string }) {
13+
// This is mock data - in a real app you would call a weather API
14+
const mockWeather = {
15+
location,
16+
temperature: Math.round(15 + Math.random() * 20),
17+
conditions: ["sunny", "cloudy", "rainy", "partly cloudy"][
18+
Math.floor(Math.random() * 4)
19+
],
20+
humidity: Math.round(40 + Math.random() * 40),
21+
};
22+
23+
return (
24+
`Weather in ${mockWeather.location}:\n` +
25+
`Temperature: ${mockWeather.temperature}°C\n` +
26+
`Conditions: ${mockWeather.conditions}\n` +
27+
`Humidity: ${mockWeather.humidity}%`
28+
);
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { z } from "zod";
2+
import type { ToolExtraArguments } from "xmcp";
3+
4+
export const metadata = {
5+
name: "hello",
6+
description: "Says hello to a user",
7+
};
8+
9+
export const schema = {
10+
name: z.string().describe("The name to greet"),
11+
};
12+
13+
export default async function hello(
14+
{ name }: { name: string },
15+
extra: ToolExtraArguments
16+
) {
17+
const greeting = `Hello, ${name}! From Cloudflare Workers.`;
18+
19+
if (extra.authInfo) {
20+
return `${greeting}\n(Authenticated as: ${extra.authInfo.clientId})`;
21+
}
22+
23+
return greeting;
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"lib": ["ES2022"],
7+
"types": ["@cloudflare/workers-types"],
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"forceConsistentCasingInFileNames": true,
12+
"outDir": "dist",
13+
"declaration": true,
14+
"declarationMap": true,
15+
"sourceMap": true
16+
},
17+
"include": ["src/**/*", "xmcp-env.d.ts"],
18+
"exclude": ["node_modules", "dist"]
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { XmcpConfig } from "xmcp";
2+
3+
const config: XmcpConfig = {
4+
http: {
5+
debug: true,
6+
},
7+
paths: {
8+
tools: "./src/tools",
9+
prompts: false,
10+
resources: false,
11+
},
12+
};
13+
14+
export default config;

examples/with-nestjs/tsconfig.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"noFallthroughCasesInSwitch": false,
2020
"esModuleInterop": true,
2121
"paths": {
22-
"@xmcp/*": ["./.xmcp/*"]
23-
}
22+
"@xmcp/*": ["./.xmcp/*"],
23+
},
2424
},
25-
"include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"]
25+
"include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"],
2626
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import path from "path";
2+
import fs from "fs-extra";
3+
4+
const DEFAULT_WRANGLER_VERSION = "^4.61.1";
5+
const DEFAULT_WORKERS_TYPES_VERSION = "^4.20241112.0";
6+
7+
function sanitizeWorkerName(name: string): string {
8+
return name
9+
.replace(/^@[^/]+\//, "")
10+
.replace(/[^a-zA-Z0-9-]/g, "-")
11+
.toLowerCase();
12+
}
13+
14+
function getProjectName(projectPath: string): string {
15+
try {
16+
const packageJsonPath = path.join(projectPath, "package.json");
17+
if (fs.existsSync(packageJsonPath)) {
18+
const packageJson = fs.readJsonSync(packageJsonPath);
19+
if (packageJson.name) {
20+
return sanitizeWorkerName(packageJson.name);
21+
}
22+
}
23+
} catch {
24+
// fall back to directory name
25+
}
26+
27+
return sanitizeWorkerName(path.basename(projectPath));
28+
}
29+
30+
function ensureWranglerConfig(projectPath: string): void {
31+
const wranglerTomlPath = path.join(projectPath, "wrangler.toml");
32+
const wranglerJsoncPath = path.join(projectPath, "wrangler.jsonc");
33+
if (fs.existsSync(wranglerTomlPath) || fs.existsSync(wranglerJsoncPath)) {
34+
return;
35+
}
36+
37+
const projectName = getProjectName(projectPath);
38+
const compatibilityDate = new Date().toISOString().split("T")[0];
39+
40+
const wranglerConfig = `{
41+
"$schema": "node_modules/wrangler/config-schema.json",
42+
// Wrangler config generated by: create-xmcp-app --cloudflare
43+
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
44+
"name": ${JSON.stringify(projectName)},
45+
"main": "worker.js",
46+
"compatibility_date": ${JSON.stringify(compatibilityDate)},
47+
"compatibility_flags": ["nodejs_compat"],
48+
49+
// Observability (Workers Logs)
50+
"observability": {
51+
"enabled": true
52+
}
53+
}
54+
`;
55+
56+
fs.writeFileSync(wranglerJsoncPath, wranglerConfig);
57+
}
58+
59+
function ensureTsConfig(projectPath: string): void {
60+
const tsconfigPath = path.join(projectPath, "tsconfig.json");
61+
if (!fs.existsSync(tsconfigPath)) {
62+
return;
63+
}
64+
65+
const tsconfig = fs.readJsonSync(tsconfigPath);
66+
tsconfig.compilerOptions = tsconfig.compilerOptions ?? {};
67+
68+
if (!Array.isArray(tsconfig.compilerOptions.types)) {
69+
return;
70+
}
71+
72+
const types: string[] = tsconfig.compilerOptions.types;
73+
74+
if (!types.includes("@cloudflare/workers-types")) {
75+
types.push("@cloudflare/workers-types");
76+
}
77+
78+
tsconfig.compilerOptions.types = types;
79+
80+
fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
81+
}
82+
83+
export function applyCloudflareSettings(projectPath: string): void {
84+
const packageJsonPath = path.join(projectPath, "package.json");
85+
if (fs.existsSync(packageJsonPath)) {
86+
const packageJson = fs.readJsonSync(packageJsonPath);
87+
88+
packageJson.scripts = packageJson.scripts ?? {};
89+
packageJson.scripts.build = "xmcp build --cf";
90+
if (!packageJson.scripts.dev || packageJson.scripts.dev === "xmcp dev") {
91+
packageJson.scripts.dev =
92+
"concurrently \"xmcp dev --cf\" \"npx wrangler dev\"";
93+
}
94+
packageJson.scripts.deploy =
95+
packageJson.scripts.deploy ?? "npx wrangler deploy";
96+
packageJson.scripts.preview =
97+
packageJson.scripts.preview ?? "npx wrangler dev";
98+
99+
if (!packageJson.type) {
100+
packageJson.type = "module";
101+
}
102+
103+
packageJson.devDependencies = packageJson.devDependencies ?? {};
104+
packageJson.devDependencies.concurrently =
105+
packageJson.devDependencies.concurrently ?? "^9.2.0";
106+
packageJson.devDependencies.wrangler =
107+
packageJson.devDependencies.wrangler ?? DEFAULT_WRANGLER_VERSION;
108+
packageJson.devDependencies["@cloudflare/workers-types"] =
109+
packageJson.devDependencies["@cloudflare/workers-types"] ??
110+
DEFAULT_WORKERS_TYPES_VERSION;
111+
112+
fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
113+
}
114+
115+
ensureWranglerConfig(projectPath);
116+
ensureTsConfig(projectPath);
117+
}

packages/create-xmcp-app/src/helpers/create.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { renameFiles } from "./rename.js";
66
import { updatePackageJson } from "./update-package.js";
77
import { install } from "./install.js";
88
import { generateConfig } from "./generate-config.js";
9+
import { applyCloudflareSettings } from "./cloudflare.js";
910

1011
const __filename = fileURLToPath(import.meta.url);
1112
const __dirname = path.dirname(__filename);
@@ -20,6 +21,7 @@ interface ProjectOptions {
2021
paths?: string[];
2122
template?: string;
2223
tailwind?: boolean;
24+
cloudflare?: boolean;
2325
}
2426

2527
/**
@@ -45,6 +47,7 @@ export function createProject(options: ProjectOptions): void {
4547
paths = ["tools", "prompts", "resources"],
4648
template = "typescript",
4749
tailwind = false,
50+
cloudflare = false,
4851
} = options;
4952

5053
// Ensure the project directory exists
@@ -76,6 +79,10 @@ export function createProject(options: ProjectOptions): void {
7679
// Update package.json with project configuration
7780
updatePackageJson(projectPath, projectName, transports);
7881

82+
if (cloudflare) {
83+
applyCloudflareSettings(projectPath);
84+
}
85+
7986
// Create necessary project directories
8087
createProjectDirectories(projectPath);
8188

0 commit comments

Comments
 (0)