Skip to content

Commit 9384880

Browse files
Simplify Remote MCP example Hono app (#27)
The goal of this demo app is to serve as a simplified example that's easy to follow. The code for the example was largely generated by Claude and spread the app code across a bunch of files and interleaved static html content with the authentication logic. It was pretty hard to follow. This puts all of the logic for the Hono app into one file, makes it clear where you would add your own user / password validation logic, and hides away all of the incidental HTML generation in the utils.ts file.
1 parent f2c4458 commit 9384880

File tree

14 files changed

+4927
-5385
lines changed

14 files changed

+4927
-5385
lines changed

demos/remote-mcp-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ When you open Claude a browser window should open and allow you to login. You sh
6969

7070
## Deploy to Cloudflare
7171

72-
1. `npx wrangler@latest kv namespace create remote-mcp-server-oauth-kv`
72+
1. `npx wrangler kv namespace create OAUTH_KV`
7373
2. Follow the guidance to add the kv namespace ID to `wrangler.jsonc`
7474
3. `npm run deploy`
7575

demos/remote-mcp-server/src/app.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Hono } from "hono";
2+
import {
3+
layout,
4+
homeContent,
5+
parseApproveFormBody,
6+
renderAuthorizationRejectedContent,
7+
renderAuthorizationApprovedContent,
8+
renderLoggedInAuthorizeScreen,
9+
renderLoggedOutAuthorizeScreen,
10+
} from "./utils";
11+
import type { OAuthHelpers } from "@cloudflare/workers-oauth-provider";
12+
13+
export type Bindings = Env & {
14+
OAUTH_PROVIDER: OAuthHelpers;
15+
};
16+
17+
const app = new Hono<{
18+
Bindings: Bindings;
19+
}>();
20+
21+
// Render a basic homepage placeholder to make sure the app is up
22+
app.get("/", async (c) => {
23+
const content = await homeContent(c.req.raw);
24+
return c.html(layout(content, "MCP Remote Auth Demo - Home"));
25+
});
26+
27+
// Render an authorization page
28+
// If the user is logged in, we'll show a form to approve the appropriate scopes
29+
// If the user is not logged in, we'll show a form to both login and approve the scopes
30+
app.get("/authorize", async (c) => {
31+
// We don't have an actual auth system, so to demonstrate both paths, you can
32+
// hard-code whether the user is logged in or not. We'll default to true
33+
// const isLoggedIn = false;
34+
const isLoggedIn = true;
35+
36+
const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
37+
38+
const oauthScopes = [
39+
{
40+
name: "read_profile",
41+
description: "Read your basic profile information",
42+
},
43+
{ name: "read_data", description: "Access your stored data" },
44+
{ name: "write_data", description: "Create and modify your data" },
45+
];
46+
47+
if (isLoggedIn) {
48+
const content = await renderLoggedInAuthorizeScreen(oauthScopes, oauthReqInfo);
49+
return c.html(layout(content, "MCP Remote Auth Demo - Authorization"));
50+
}
51+
52+
const content = await renderLoggedOutAuthorizeScreen(oauthScopes, oauthReqInfo);
53+
return c.html(layout(content, "MCP Remote Auth Demo - Authorization"));
54+
});
55+
56+
// The /authorize page has a form that will POST to /approve
57+
// This endpoint is responsible for validating any login information and
58+
// then completing the authorization request with the OAUTH_PROVIDER
59+
app.post("/approve", async (c) => {
60+
const { action, oauthReqInfo, email, password } = await parseApproveFormBody(
61+
await c.req.parseBody(),
62+
);
63+
64+
if (!oauthReqInfo) {
65+
return c.html("INVALID LOGIN", 401);
66+
}
67+
68+
// If the user needs to both login and approve, we should validate the login first
69+
if (action === "login_approve") {
70+
// We'll allow any values for email and password for this demo
71+
// but you could validate them here
72+
// Ex:
73+
// if (email !== "[email protected]" || password !== "password") {
74+
// biome-ignore lint/correctness/noConstantCondition: This is a demo
75+
if (false) {
76+
return c.html(
77+
layout(
78+
await renderAuthorizationRejectedContent("/"),
79+
"MCP Remote Auth Demo - Authorization Status",
80+
),
81+
);
82+
}
83+
}
84+
85+
// The user must be successfully logged in and have approved the scopes, so we
86+
// can complete the authorization request
87+
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
88+
request: oauthReqInfo,
89+
userId: email,
90+
metadata: {
91+
label: "Test User",
92+
},
93+
scope: oauthReqInfo.scope,
94+
props: {
95+
userEmail: email,
96+
},
97+
});
98+
99+
return c.html(
100+
layout(
101+
await renderAuthorizationApprovedContent(redirectTo),
102+
"MCP Remote Auth Demo - Authorization Status",
103+
),
104+
);
105+
});
106+
107+
export default app;

demos/remote-mcp-server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import app from "./routes";
1+
import app from "./app";
22
import { DurableMCP } from "workers-mcp";
33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { z } from "zod";

demos/remote-mcp-server/src/routes/_app.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

demos/remote-mcp-server/src/routes/_middleware.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

demos/remote-mcp-server/src/routes/approve.ts

Lines changed: 0 additions & 91 deletions
This file was deleted.

demos/remote-mcp-server/src/routes/authorize.ts

Lines changed: 0 additions & 144 deletions
This file was deleted.

0 commit comments

Comments
 (0)