Skip to content
Open
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
5 changes: 4 additions & 1 deletion examples/analytics-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"concurrently": "^9.1.2",
"typescript": "catalog:",
"vitest": "catalog:",
"wrangler": "catalog:"
"wrangler": "catalog:",
"http-errors": "^2.0.0",
"raw-body": "^3.0.1",
"statuses": "^2.0.2"
}
}
4 changes: 3 additions & 1 deletion examples/analytics-mcp/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createMcpWorkersConfig } from "@nullshot/test-utils/vitest/mcpWorkersConfig";

export default createMcpWorkersConfig();
export default createMcpWorkersConfig({
test: { deps: { optimizer: { ssr: { include: ["http-errors"] } } } },
});
9 changes: 6 additions & 3 deletions examples/browser-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
"zod": "catalog:"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.8.68",
"@cloudflare/workers-types": "^4.20241218.0",
"@cloudflare/vitest-pool-workers": "catalog:default",
"@cloudflare/workers-types": "catalog:default",
"@nullshot/test-utils": "workspace:*",
"@types/node": "^22.10.2",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"typescript": "^5.7.2",
"vitest": "^2.1.8",
"wrangler": "^4.33.1"
"wrangler": "^4.33.1",
"http-errors": "^2.0.0",
"raw-body": "^3.0.1",
"statuses": "^2.0.2"
},
"optionalDependencies": {
"puppeteer": "^23.11.1"
Expand Down
8 changes: 4 additions & 4 deletions examples/browser-mcp/test/browser-mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "cloudflare:test";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { WorkerSSEClientTransport } from "@nullshot/test-utils/mcp/WorkerSSEClientTransport";
import { WorkerStreamableHTTPClientTransport } from "@nullshot/test-utils/mcp/WorkerStreamableHTTPClientTransport";

// Define response types for clarity
interface ToolResponse {
Expand Down Expand Up @@ -84,8 +84,8 @@ describe("Browser MCP Client Integration Tests", () => {

// Helper function to create the transport
function createTransport(ctx: ExecutionContext) {
const url = new URL(`${baseUrl}/sse`);
return new WorkerSSEClientTransport(url, ctx);
const url = new URL(`${baseUrl}/mcp`);
return new WorkerStreamableHTTPClientTransport(url, ctx);
}

// Helper function to check if a tool call involves browser rendering
Expand Down Expand Up @@ -124,7 +124,7 @@ describe("Browser MCP Client Integration Tests", () => {
});

it("should successfully connect to the browser MCP server", async () => {
console.log(`Testing SSE transport connection`);
console.log(`Testing StreamableHTTP transport connection`);

const transport = createTransport(ctx);
await client.connect(transport);
Expand Down
4 changes: 3 additions & 1 deletion examples/browser-mcp/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createMcpWorkersConfig } from "@nullshot/test-utils/vitest/mcpWorkersConfig";

export default createMcpWorkersConfig();
export default createMcpWorkersConfig({
test: { deps: { optimizer: { ssr: { include: ["http-errors"] } } } },
});
5 changes: 4 additions & 1 deletion examples/crud-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
"concurrently": "^9.1.2",
"typescript": "catalog:",
"vitest": "catalog:",
"wrangler": "catalog:"
"wrangler": "catalog:",
"http-errors": "^2.0.0",
"raw-body": "^3.0.1",
"statuses": "^2.0.2"
},
"dependencies": {
"@modelcontextprotocol/sdk": "catalog:",
Expand Down
15 changes: 8 additions & 7 deletions examples/crud-mcp/test/todo-mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "cloudflare:test";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { WorkerSSEClientTransport } from "@nullshot/test-utils/mcp/WorkerSSEClientTransport";
import { WorkerStreamableHTTPClientTransport } from "@nullshot/test-utils/mcp/WorkerStreamableHTTPClientTransport";
import { TodoStatus, Todo } from "../src/schema";

// Define response type for clarity
Expand Down Expand Up @@ -62,8 +62,8 @@ describe("Todo MCP Client Integration Tests", () => {

// Helper function to create the transport
function createTransport(ctx: ExecutionContext) {
const url = new URL(`${baseUrl}/sse`);
return new WorkerSSEClientTransport(url, ctx);
const url = new URL(`${baseUrl}/mcp`);
return new WorkerStreamableHTTPClientTransport(url, ctx);
}

// Test for basic functionality
Expand All @@ -77,10 +77,11 @@ describe("Todo MCP Client Integration Tests", () => {
});

it("should successfully connect to the todo MCP server", async () => {
console.log(`Testing SSE transport connection`);
console.log(`Testing StreamableHTTP transport connection`);

const transport = createTransport(ctx);
await client.connect(transport);
console.log("Connected to transport");

await waitOnExecutionContext(ctx);
console.log(`Client connection test passed!`);
Expand Down Expand Up @@ -381,7 +382,7 @@ describe("Todo MCP Client Integration Tests", () => {
} catch (error) {
// If the resource is not available, create a manual test success
console.log(
"listTodos resource not available, skipping detailed assertions"
"listTodos resource not available, skipping detailed assertions",
);
expect(pendingTodoResponse.todo?.status).toBe(TodoStatus.NOT_STARTED);
expect(inProgressTodoResponse.todo?.status).toBe(TodoStatus.IN_PROGRESS);
Expand Down Expand Up @@ -427,7 +428,7 @@ describe("Todo MCP Client Integration Tests", () => {
} catch (error) {
// If resource approach fails, use the tool call approach as fallback
console.log(
"d1://database/todos/stats resource not available, using tool instead"
"d1://database/todos/stats resource not available, using tool instead",
);
// Original tool approach
const statsResponse = (await client.callTool({
Expand Down Expand Up @@ -527,7 +528,7 @@ describe("Todo MCP Client Integration Tests", () => {
} catch (error) {
// If the search resource is not available, we'll simply pass the test
console.log(
"listTodos resource with search_text not available, skipping detailed assertions"
"listTodos resource with search_text not available, skipping detailed assertions",
);
}

Expand Down
4 changes: 3 additions & 1 deletion examples/crud-mcp/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createMcpWorkersConfig } from "@nullshot/test-utils/vitest/mcpWorkersConfig";

export default createMcpWorkersConfig();
export default createMcpWorkersConfig({
test: { deps: { optimizer: { ssr: { include: ["http-errors"] } } } },
});
5 changes: 4 additions & 1 deletion examples/dependent-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
"@nullshot/cli": "workspace:*",
"@types/node": "catalog:",
"typescript": "catalog:",
"wrangler": "catalog:"
"wrangler": "catalog:",
"http-errors": "^2.0.0",
"raw-body": "^3.0.1",
"statuses": "^2.0.2"
},
"mcpServers": {
"mcp-template": {
Expand Down
36 changes: 25 additions & 11 deletions examples/email-mcp/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Email MCP (Durable Object SQL DB + Cloudflare Email)

An MCP server that:

- Sends internal emails via Cloudflare's Email binding (only to verified addresses).
- Manages email data via Durable Object SQLite storage.
- Exposes MCP tools and resources to interact with emails.

Features

- Tools:
- create_test_email(from_addr, to_addr, subject, text) - Create test emails for database testing
- send_email(to, subject, text) - Send real emails via Cloudflare Email Workers (requires domain setup)
Expand All @@ -16,39 +18,47 @@ Features
- do://database/emails/{id} - Get specific email

Important limitations

- This is for internal email only. Cloudflare’s Send Email binding delivers only to verified recipients on your zone.
- Not a general outbound SMTP service.

Setup

1) Durable Object SQL Database
1. Durable Object SQL Database

- No external database setup required! The Durable Object has built-in SQLite storage.
- Email data is stored in the Durable Object's persistent SQLite database.

2) Email Routing and bindings
2. Email Routing and bindings

- Verify MAIL_FROM (sender) address in Cloudflare Email Routing.
- Verify intended recipient addresses or domains (ALLOWED_RECIPIENTS).
- Add “Send Email” binding named SEND_EMAIL in wrangler.jsonc.

3) Durable Object binding
3. Durable Object binding

- EMAIL_MCP_SERVER is defined in wrangler.jsonc. No extra setup beyond deploy.

4) Env vars
4. Env vars

- MAIL_FROM: the verified sender (e.g., no-reply@example.com)
- ALLOWED_RECIPIENTS: comma-separated emails or @domain rules. Examples:
- "alice@example.com,bob@example.com,@example.com"

Local development

- Install deps: pnpm i
- Update wrangler.jsonc vars and bindings.
- Run: pnpm dev

Deploy

- pnpm deploy

Testing MCP with the Playground

- Start the Playground package or your client.
- Connect via SSE/WebSocket to this Worker’s /sse (the MCP package already mounts standard endpoints in the DO).
- Connect via HTTP/SSE to this Worker’s /mcp (the MCP package already mounts standard endpoints in the DO).
- Call tools:
- send_email
- list_emails
Expand All @@ -60,9 +70,9 @@ Testing the MCP Server

You can test the core email management functionality immediately:

1. **Connect to MCP server**:
- Local: http://localhost:PORT/sse (where PORT is shown by wrangler dev)
- Production: https://your-worker.workers.dev/sse
1. **Connect to MCP server**:
- Local: http://localhost:PORT/mcp (where PORT is shown by wrangler dev)
- Production: https://your-worker.workers.dev/mcp

2. **Create test emails**:
- Tool: `create_test_email`
Expand All @@ -76,7 +86,7 @@ You can test the core email management functionality immediately:
- Copy any full UUID for detailed viewing

4. **Get specific email**:
- Tool: `get_email`
- Tool: `get_email`
- Use: Any UUID from the list_emails results
- Shows: Complete email details including full content

Expand All @@ -85,13 +95,15 @@ You can test the core email management functionality immediately:
To test the `send_email` tool for actual email delivery:

### Prerequisites:

1. **Own a domain** registered with any provider
2. **Add domain to Cloudflare** and enable Email Routing
3. **Verify sender addresses** in Cloudflare Email Routing → Send tab
4. **Update MAIL_FROM** in wrangler.jsonc to your verified domain
5. **Update ALLOWED_RECIPIENTS** with addresses you want to allow

### Setup Steps:

1. **Cloudflare Dashboard** → Your Domain → Email → Email Routing
2. **Enable Email Routing** (adds required DNS records automatically)
3. **Add destination addresses** and verify them via email
Expand All @@ -107,12 +119,14 @@ To test the `send_email` tool for actual email delivery:
7. **Test**: Use `send_email` tool with allowed recipients

### Expected Results:

- **✅ Allowed recipients**: Email sent successfully
- **❌ Disallowed recipients**: "Recipient not allowed" error
- **❌ Unverified sender domain**: "domain not owned" error


Notes

- Large raw messages: we store only the text and metadata in the Durable Object SQLite database. If you need full raw message storage, consider R2.
- If you see recipient not allowed, update ALLOWED_RECIPIENTS.
- The Durable Object SQLite database provides strong consistency and is automatically managed by Cloudflare.
- The Durable Object SQLite database provides strong consistency and is automatically managed by Cloudflare.

7 changes: 5 additions & 2 deletions examples/email-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "catalog:",
"@types/node": "catalog:",
"@nullshot/test-utils": "workspace:*",
"@types/chai": "^5.2.2",
"@types/node": "catalog:",
"chai": "^6.0.1",
"concurrently": "^9.1.2",
"typescript": "catalog:",
"vitest": "catalog:",
"http-errors": "^2.0.0",
"raw-body": "^3.0.1",
"statuses": "^2.0.2",
"wrangler": "catalog:"
},
"dependencies": {
"@modelcontextprotocol/sdk": "catalog:",
"@nullshot/mcp": "workspace:*",
"zod": "catalog:"
}
}
}
27 changes: 18 additions & 9 deletions examples/email-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { EmailMcpServer } from './server';
import { EmailMcpServer } from "./server";

export { EmailMcpServer };

// Worker entrypoint for handling requests and email events.
// We shard by sessionId if provided, else by a stable name to avoid too many DOs.
export default {
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
async fetch(
request: Request,
env: Env,
_ctx: ExecutionContext,
): Promise<Response> {
const url = new URL(request.url);
let sessionIdStr = url.searchParams.get('sessionId');

const id = sessionIdStr
? env.EMAIL_MCP_SERVER.idFromString(sessionIdStr)
: env.EMAIL_MCP_SERVER.idFromName('default-email-session');
// Dynamically generate sessionId if it isn't provided to allocate a session
const sessionId =
request.headers.get("mcp-session-id") ?? crypto.randomUUID();

url.searchParams.set('sessionId', id.toString());
const reqClone = request.clone();

const json = await request.json();

console.log("Request:", { headers: request.headers, json });

const id = env.EMAIL_MCP_SERVER.idFromName(sessionId);

return env.EMAIL_MCP_SERVER.get(id).fetch(
new Request(url.toString(), request)
new Request(url.toString(), reqClone),
);
},
};
};
Loading
Loading