Skip to content
Merged

0.6.3 #488

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/cloudflare-workers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Dependencies
node_modules/

# Build output
dist/
.xmcp/
worker.js
wrangler.jsonc

# Environment variables (contains secrets)
.dev.vars

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db
70 changes: 70 additions & 0 deletions examples/cloudflare-workers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Cloudflare Workers Example

This example demonstrates how to deploy an xmcp MCP server to Cloudflare Workers.

## Getting Started

1. Install dependencies:

```bash
pnpm install
```

2. Build for Cloudflare Workers:

```bash
pnpm build
```

This writes these files into the project root:

- `worker.js` - The bundled Cloudflare Worker
- `wrangler.jsonc` - Wrangler configuration template (only if you don't already have `wrangler.toml/jsonc`)

3. Start development mode (watch + rebuild worker output) and Wrangler:

```bash
pnpm dev
```

This runs `xmcp dev --cf` (rebuilds `worker.js`) and `wrangler dev` together.

4. Test with curl:

```bash
pnpm preview
# or
npx wrangler dev
```

```bash
# Health check
curl http://localhost:8787/health

# List tools
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

# Call a tool
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"hello","arguments":{"name":"World"}},"id":2}'
```

5. Deploy to Cloudflare:

```bash
pnpm deploy
# or
npx wrangler deploy
```

## Notes

- The `--cf` flag builds a Cloudflare Workers-native bundle
- All Node.js APIs are bundled into the worker (no external dependencies)
- React component bundles are inlined at compile time
- The worker uses Web APIs only (no Node.js runtime dependencies)
23 changes: 23 additions & 0 deletions examples/cloudflare-workers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "cloudflare-workers-example",
"version": "0.0.1",
"description": "Example xmcp server deployed to Cloudflare Workers",
"private": true,
"type": "module",
"scripts": {
"build": "xmcp build --cf",
"dev": "concurrently \"xmcp dev --cf\" \"npx wrangler dev\"",
"deploy": "npx wrangler deploy",
"preview": "npx wrangler dev"
},
"dependencies": {
"xmcp": "workspace:*",
"zod": "^3.23.8"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241112.0",
"concurrently": "^9.2.0",
"typescript": "^5.7.2",
"wrangler": "^4.62.0"
}
}
29 changes: 29 additions & 0 deletions examples/cloudflare-workers/src/tools/get-weather.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from "zod";

export const metadata = {
name: "get-weather",
description: "Get the current weather for a location (mock data)",
};

export const schema = {
location: z.string().describe("The city or location to get weather for"),
};

export default async function getWeather({ location }: { location: string }) {
// This is mock data - in a real app you would call a weather API
const mockWeather = {
location,
temperature: Math.round(15 + Math.random() * 20),
conditions: ["sunny", "cloudy", "rainy", "partly cloudy"][
Math.floor(Math.random() * 4)
],
humidity: Math.round(40 + Math.random() * 40),
};

return (
`Weather in ${mockWeather.location}:\n` +
`Temperature: ${mockWeather.temperature}°C\n` +
`Conditions: ${mockWeather.conditions}\n` +
`Humidity: ${mockWeather.humidity}%`
);
Comment on lines +23 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For improved readability and maintainability, consider using an array of strings joined by a newline character instead of string concatenation with +. This pattern is often cleaner when building multi-line strings.

Suggested change
return (
`Weather in ${mockWeather.location}:\n` +
`Temperature: ${mockWeather.temperature}°C\n` +
`Conditions: ${mockWeather.conditions}\n` +
`Humidity: ${mockWeather.humidity}%`
);
return [
`Weather in ${mockWeather.location}:`,
`Temperature: ${mockWeather.temperature}°C`,
`Conditions: ${mockWeather.conditions}`,
`Humidity: ${mockWeather.humidity}%`,
].join("\n");

}
24 changes: 24 additions & 0 deletions examples/cloudflare-workers/src/tools/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from "zod";
import type { ToolExtraArguments } from "xmcp";

export const metadata = {
name: "hello",
description: "Says hello to a user",
};

export const schema = {
name: z.string().describe("The name to greet"),
};

export default async function hello(
{ name }: { name: string },
extra: ToolExtraArguments
) {
const greeting = `Hello, ${name}! From Cloudflare Workers.`;

if (extra.authInfo) {
return `${greeting}\n(Authenticated as: ${extra.authInfo.clientId})`;
}

return greeting;
}
19 changes: 19 additions & 0 deletions examples/cloudflare-workers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*", "xmcp-env.d.ts"],
"exclude": ["node_modules", "dist"]
}
14 changes: 14 additions & 0 deletions examples/cloudflare-workers/xmcp.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { XmcpConfig } from "xmcp";

const config: XmcpConfig = {
http: {
debug: true,
},
paths: {
tools: "./src/tools",
prompts: false,
resources: false,
},
};

export default config;
6 changes: 3 additions & 3 deletions examples/with-nestjs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true,
"paths": {
"@xmcp/*": ["./.xmcp/*"]
}
"@xmcp/*": ["./.xmcp/*"],
},
},
"include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"]
"include": ["src/**/*", "xmcp-env.d.ts", ".xmcp/**/*"],
}
117 changes: 117 additions & 0 deletions packages/create-xmcp-app/src/helpers/cloudflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import path from "path";
import fs from "fs-extra";

const DEFAULT_WRANGLER_VERSION = "^4.61.1";
const DEFAULT_WORKERS_TYPES_VERSION = "^4.20241112.0";

function sanitizeWorkerName(name: string): string {
return name
.replace(/^@[^/]+\//, "")
.replace(/[^a-zA-Z0-9-]/g, "-")
.toLowerCase();
}

function getProjectName(projectPath: string): string {
try {
const packageJsonPath = path.join(projectPath, "package.json");
if (fs.existsSync(packageJsonPath)) {
const packageJson = fs.readJsonSync(packageJsonPath);
if (packageJson.name) {
return sanitizeWorkerName(packageJson.name);
}
}
} catch {
// fall back to directory name
}

return sanitizeWorkerName(path.basename(projectPath));
}

function ensureWranglerConfig(projectPath: string): void {
const wranglerTomlPath = path.join(projectPath, "wrangler.toml");
const wranglerJsoncPath = path.join(projectPath, "wrangler.jsonc");
if (fs.existsSync(wranglerTomlPath) || fs.existsSync(wranglerJsoncPath)) {
return;
}

const projectName = getProjectName(projectPath);
const compatibilityDate = new Date().toISOString().split("T")[0];

const wranglerConfig = `{
"$schema": "node_modules/wrangler/config-schema.json",
// Wrangler config generated by: create-xmcp-app --cloudflare
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
"name": ${JSON.stringify(projectName)},
"main": "worker.js",
"compatibility_date": ${JSON.stringify(compatibilityDate)},
"compatibility_flags": ["nodejs_compat"],

// Observability (Workers Logs)
"observability": {
"enabled": true
}
}
`;

fs.writeFileSync(wranglerJsoncPath, wranglerConfig);
}

function ensureTsConfig(projectPath: string): void {
const tsconfigPath = path.join(projectPath, "tsconfig.json");
if (!fs.existsSync(tsconfigPath)) {
return;
}

const tsconfig = fs.readJsonSync(tsconfigPath);
tsconfig.compilerOptions = tsconfig.compilerOptions ?? {};

if (!Array.isArray(tsconfig.compilerOptions.types)) {
return;
}
Comment on lines +68 to +70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic in ensureTsConfig will not add @cloudflare/workers-types to tsconfig.json if the compilerOptions.types array does not already exist. The function returns early if tsconfig.compilerOptions.types is not an array, which includes the case where it is undefined. This can lead to an incomplete setup for users who don't have a types array in their tsconfig.json.

The logic should be adjusted to handle cases where types is missing and create it, or when it exists, to add the new type if it's not already present.

Suggested change
if (!Array.isArray(tsconfig.compilerOptions.types)) {
return;
}
if (tsconfig.compilerOptions.types === undefined) {
tsconfig.compilerOptions.types = [];
} else if (!Array.isArray(tsconfig.compilerOptions.types)) {
// If `types` exists but is not an array, we can't safely modify it.
return;
}


const types: string[] = tsconfig.compilerOptions.types;

if (!types.includes("@cloudflare/workers-types")) {
types.push("@cloudflare/workers-types");
}

tsconfig.compilerOptions.types = types;

fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
}

export function applyCloudflareSettings(projectPath: string): void {
const packageJsonPath = path.join(projectPath, "package.json");
if (fs.existsSync(packageJsonPath)) {
const packageJson = fs.readJsonSync(packageJsonPath);

packageJson.scripts = packageJson.scripts ?? {};
packageJson.scripts.build = "xmcp build --cf";
if (!packageJson.scripts.dev || packageJson.scripts.dev === "xmcp dev") {
packageJson.scripts.dev =
"concurrently \"xmcp dev --cf\" \"npx wrangler dev\"";
}
packageJson.scripts.deploy =
packageJson.scripts.deploy ?? "npx wrangler deploy";
packageJson.scripts.preview =
packageJson.scripts.preview ?? "npx wrangler dev";

if (!packageJson.type) {
packageJson.type = "module";
}

packageJson.devDependencies = packageJson.devDependencies ?? {};
packageJson.devDependencies.concurrently =
packageJson.devDependencies.concurrently ?? "^9.2.0";
packageJson.devDependencies.wrangler =
packageJson.devDependencies.wrangler ?? DEFAULT_WRANGLER_VERSION;
packageJson.devDependencies["@cloudflare/workers-types"] =
packageJson.devDependencies["@cloudflare/workers-types"] ??
DEFAULT_WORKERS_TYPES_VERSION;

fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
}

ensureWranglerConfig(projectPath);
ensureTsConfig(projectPath);
}
7 changes: 7 additions & 0 deletions packages/create-xmcp-app/src/helpers/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { renameFiles } from "./rename.js";
import { updatePackageJson } from "./update-package.js";
import { install } from "./install.js";
import { generateConfig } from "./generate-config.js";
import { applyCloudflareSettings } from "./cloudflare.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -20,6 +21,7 @@ interface ProjectOptions {
paths?: string[];
template?: string;
tailwind?: boolean;
cloudflare?: boolean;
}

/**
Expand All @@ -45,6 +47,7 @@ export function createProject(options: ProjectOptions): void {
paths = ["tools", "prompts", "resources"],
template = "typescript",
tailwind = false,
cloudflare = false,
} = options;

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

if (cloudflare) {
applyCloudflareSettings(projectPath);
}

// Create necessary project directories
createProjectDirectories(projectPath);

Expand Down
Loading