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
3 changes: 3 additions & 0 deletions demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"./demos/mcp-stytch-consumer-todo-list": {
"package_json_hash": "428d6caecb4991a0847a8a77aaf791a6885fe72e"
},
"./demos/mcp-scalekit-todo-list": {
"package_json_hash": "b334e7d46f47cd6e9e0ddb4d8b15dc51a98ceab8"
},
"./demos/model-scraper": {
"package_json_hash": "361b037f96d7eefd23df44059d8313ade0bd28fc"
},
Expand Down
4 changes: 4 additions & 0 deletions demos/mcp-scalekit-todo-list/.dev.vars.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SCALEKIT_CLIENT_ID=TODO_FROM_SCALEKIT_DASHBOARD
SCALEKIT_CLIENT_SECRET=TODO_FROM_SCALEKIT_DASHBOARD
SCALEKIT_ENVIRONMENT_URL=TODO_FROM_SCALEKIT_DASHBOARD

2 changes: 2 additions & 0 deletions demos/mcp-scalekit-todo-list/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_SCALEKIT_CLIENT_ID=TODO_FROM_SCALEKIT_DASHBOARD
VITE_SCALEKIT_ENVIRONMENT_URL=TODO_FROM_SCALEKIT_DASHBOARD
29 changes: 29 additions & 0 deletions demos/mcp-scalekit-todo-list/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# wrangler files
.wrangler
.dev.vars*
!.dev.vars.template
21 changes: 21 additions & 0 deletions demos/mcp-scalekit-todo-list/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Scalekit Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
144 changes: 144 additions & 0 deletions demos/mcp-scalekit-todo-list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Workers + Scalekit TODO App MCP Server

This is a Workers server that composes three functions:
* A static website built using React and Vite on top of [Worker Assets](https://developers.cloudflare.com/workers/static-assets/)
* A REST API built using Hono on top of [Workers KV](https://developers.cloudflare.com/kv/)
* A [Model Context Protocol](https://modelcontextprotocol.io/introduction) Server built using on top of [Workers Durable Objects](https://developers.cloudflare.com/durable-objects/)

User and client identity is managed using [Scalekit](https://scalekit.com/). Put together, these three features show how to extend a traditional full-stack application for use by an AI agent.

This demo uses [Scalekit's MCP Auth](https://docs.scalekit.com/authenticate/mcp/overview/) product, which provides OAuth 2.1 authorization for MCP servers with support for dynamic client registration and CIMD (Client-Initiated Metadata Discovery).

> [!WARNING]
> This is a demo template designed to help you get started quickly. While we have implemented several security controls, **you must implement all preventive and defense-in-depth security measures before deploying to production**. Please review our comprehensive security guide: [Securing MCP Servers](https://github.com/cloudflare/agents/blob/main/docs/securing-mcp-servers.md)


## Set up

Follow the steps below to get this application fully functional and running using your own Scalekit credentials.

### In the Scalekit Dashboard

1. Create a [Scalekit](https://scalekit.com/) account and set up your organization.

2. Navigate to **Settings > API Config** in your Scalekit dashboard to retrieve your credentials:
- **Client ID**
- **Client Secret**
- **Environment URL** (e.g., `https://your-org.scalekit.com`)

3. Configure your MCP server in Scalekit:
- Register your MCP server application
- Set up OAuth 2.1 authorization endpoints
- Configure allowed redirect URIs (e.g., `http://localhost:3000/authenticate` for local development)
- Enable Dynamic Client Registration if needed

4. For detailed MCP setup instructions, see the [Scalekit MCP Auth Quickstart](https://docs.scalekit.com/authenticate/mcp/quickstart/)

### On your machine

In your terminal clone the project and install dependencies:

```bash
git clone https://github.com/cloudflare/ai.git
cd ai
npm i
cd demos/mcp-scalekit-todo-list
```

Next, create an `.env.local` file by running the command below which copies the contents of `.env.template`.

```bash
cp .env.template .env.local
```

Open `.env.local` in the text editor of your choice, and set the environment variables using your Scalekit credentials:

```env
# This is what a completed .env.local file will look like
VITE_SCALEKIT_CLIENT_ID=your_scalekit_client_id
VITE_SCALEKIT_ENVIRONMENT_URL=https://your-org.scalekit.com
```

Create a `.dev.vars` file by running the command below which copies the contents of `.dev.vars.template`.

```bash
cp .dev.vars.template .dev.vars
```

Open `.dev.vars` in the text editor of your choice, and set the environment variables using your Scalekit credentials:

```env
# This is what a completed .dev.vars file will look like
SCALEKIT_CLIENT_ID=your_scalekit_client_id
SCALEKIT_CLIENT_SECRET=your_scalekit_client_secret
SCALEKIT_ENVIRONMENT_URL=https://your-org.scalekit.com
```

## Running locally

After completing all the setup steps above the application can be run with the command:

```bash
npm run dev
```

The application will be available at [`http://localhost:3000`](http://localhost:3000) and the MCP server will be available at `http://localhost:3000/mcp`.

Test your MCP server using the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)
```bash
npx @modelcontextprotocol/inspector@latest
```

Navigate to the URL where the Inspector is running, and input the following values:
- Transport Type: `Streamable HTTP`
- URL: `http://localhost:3000/mcp`

## Deploy to Cloudflare Workers

Click the button - **you'll need to configure environment variables after the initial deployment**.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/mcp-scalekit-todo-list)

Or, if you want to follow the steps by hand:

1. Create a KV namespace for the TODO app to use

```
wrangler kv namespace create TODOS
```

2. Update the KV namespace ID in `wrangler.jsonc` with the ID you received:

```
"kv_namespaces": [
{
"binding": "TODOS",
"id": "your-kv-namespace-id"
}
]
```


3. Upload your Scalekit Env Vars for use by the worker

```bash
npx wrangler secret bulk .dev.vars
```

4. Deploy the worker

```
npm run deploy
```

5. Grant your deployment access to your Scalekit project. Assuming your deployment is at `https://mcp-scalekit-todo-list.$YOUR_ACCOUNT_NAME.workers.dev`:
1. Add `https://mcp-scalekit-todo-list.$YOUR_ACCOUNT_NAME.workers.dev/authenticate` as an allowed redirect URI in your Scalekit dashboard
2. Configure your Scalekit application to allow requests from your deployment domain

## Get help and join the community

#### 📚 Scalekit Documentation

- [MCP Auth Overview](https://docs.scalekit.com/authenticate/mcp/overview/)
- [MCP Auth Quickstart](https://docs.scalekit.com/authenticate/mcp/quickstart/)
- [Scalekit Documentation](https://docs.scalekit.com/)
32 changes: 32 additions & 0 deletions demos/mcp-scalekit-todo-list/api/TodoAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Hono} from "hono";
import {todoService} from "./TodoService.ts";
import {scalekitSessionAuthMiddleware} from "./lib/auth";


/**
* The Hono app exposes the TODO Service via REST endpoints for consumption by the frontend
*/
export const TodoAPI = new Hono<{ Bindings: Env }>()

.get('/todos', scalekitSessionAuthMiddleware, async (c) => {
const todos = await todoService(c.env, c.var.userID).get()
return c.json({todos})
})

.post('/todos', scalekitSessionAuthMiddleware, async (c) => {
const newTodo = await c.req.json<{ todoText: string }>();
const todos = await todoService(c.env, c.var.userID).add(newTodo.todoText)
return c.json({todos})
})

.post('/todos/:id/complete', scalekitSessionAuthMiddleware, async (c) => {
const todos = await todoService(c.env, c.var.userID).markCompleted(c.req.param().id)
return c.json({todos})
})

.delete('/todos/:id', scalekitSessionAuthMiddleware, async (c) => {
const todos = await todoService(c.env, c.var.userID).delete(c.req.param().id)
return c.json({todos})
})

export type TodoApp = typeof TodoAPI;
79 changes: 79 additions & 0 deletions demos/mcp-scalekit-todo-list/api/TodoMCP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {McpServer, ResourceTemplate} from '@modelcontextprotocol/sdk/server/mcp.js'
import {z} from 'zod'
import {todoService} from "./TodoService.ts";
import {AuthenticationContext, Todo} from "../types";
import {McpAgent} from "agents/mcp";

/**
* The `TodoMCP` class exposes the TODO Service via the Model Context Protocol
* for consumption by API Agents
*/
export class TodoMCP extends McpAgent<Env, unknown, AuthenticationContext> {
async init() {
}

get todoService() {
return todoService(this.env, this.props.claims.sub)
}

formatResponse = (description: string, newState: Todo[]): {
content: Array<{ type: 'text', text: string }>
} => {
return {
content: [{
type: "text",
text: `Success! ${description}\n\nNew state:\n${JSON.stringify(newState, null, 2)}}`
}]
};
}

get server() {
const server = new McpServer({
name: 'TODO Service',
version: '1.0.0',
})

server.resource("Todos", new ResourceTemplate("todoapp://todos/{id}", {
list: async () => {
const todos = await this.todoService.get()

return {
resources: todos.map(todo => ({
name: todo.text,
uri: `todoapp://todos/${todo.id}`
}))
}
}
}),
async (uri, {id}) => {
const todos = await this.todoService.get();
const todo = todos.find(todo => todo.id === id);
return {
contents: [
{
uri: uri.href,
text: todo ? `text: ${todo.text} completed: ${todo.completed}` : 'NOT FOUND',
},
],
}
},
)

server.tool('createTodo', 'Add a new TODO task', {todoText: z.string()}, async ({todoText}) => {
const todos = await this.todoService.add(todoText)
return this.formatResponse('TODO added successfully', todos)
})

server.tool('markTodoComplete', 'Mark a TODO as complete', {todoID: z.string()}, async ({todoID}) => {
const todos = await this.todoService.markCompleted(todoID)
return this.formatResponse('TODO completed successfully', todos)
})

server.tool('deleteTodo', 'Mark a TODO as deleted', {todoID: z.string()}, async ({todoID}) => {
const todos = await this.todoService.delete(todoID)
return this.formatResponse('TODO deleted successfully', todos)
})

return server
}
}
60 changes: 60 additions & 0 deletions demos/mcp-scalekit-todo-list/api/TodoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Todo} from "../types";

/**
* The `TodoService` class provides methods for managing a to-do list backed by Cloudflare KV storage.
* This includes operations such as retrieving todos, adding new todos,
* deleting existing todos, and marking todos as completed.
*/
class TodoService {
constructor(
private env: Env,
private userID: string,
) {
}

get = async (): Promise<Todo[]> => {
const todos = await this.env.TODOS.get<Todo[]>(this.userID, "json")
return todos || [];
}

#set = async (todos: Todo[]): Promise<Todo[]> => {
const sorted = todos.sort((t1, t2) => {
if (t1.completed === t2.completed) {
return t1.id.localeCompare(t2.id);
}
return t1.completed ? 1 : -1;
});

await this.env.TODOS.put(this.userID, JSON.stringify(sorted))
return sorted
}

add = async (todoText: string): Promise<Todo[]> => {
const todos = await this.get()
const newTodo: Todo = {
id: Date.now().toString(),
text: todoText,
completed: false
}
todos.push(newTodo)
return this.#set(todos)
}

delete = async (todoID: string): Promise<Todo[]> => {
const todos = await this.get()
const cleaned = todos.filter(t => t.id !== todoID);
return this.#set(cleaned);
}

markCompleted = async (todoID: string): Promise<Todo[]> => {
const todos = await this.get()
const completedTodo = todos.find(t => t.id === todoID);
if (completedTodo) {
completedTodo.completed = true;
return this.#set(todos);
}
return todos;
}
}

export const todoService = (env: Env, userID: string) => new TodoService(env, userID)
Loading