-
Notifications
You must be signed in to change notification settings - Fork 6
Add Hono utilities and example #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brettimus
wants to merge
7
commits into
clerk:main
Choose a base branch
from
brettimus:add-hono-example
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f759c3e
Add dependencies and tsup config for a hono example
brettimus d790d78
Implement a set of Hono + clerk utilities for mcp auth
brettimus e865c3d
Remove mcp-lite from package.json (not needed)
brettimus f990361
Update hono README example code note a TODO and fix an import
brettimus ce2ec94
Fix imports in hono README
brettimus 7a24cf0
Document oauth metadata cors in hono README
brettimus 58d5347
Use process.env to access CLERK_SECRET_KEY in createClerkClient
brettimus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| # MCP Tools - Hono Integration | ||
|
|
||
| Hono utilities for building MCP servers with authentication support. These tools make it easy to add MCP (Model Context Protocol) endpoints to your existing Hono applications. | ||
|
|
||
| ## Installation | ||
|
|
||
| Make sure you have the required dependencies installed: | ||
|
|
||
| ```bash | ||
| npm install @clerk/mcp-tools hono mcp-lite | ||
| ``` | ||
|
|
||
| If you're using Clerk for authentication, also install the Clerk backend SDK: | ||
|
|
||
| ```bash | ||
| npm install @clerk/backend | ||
| ``` | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### Example with Clerk Authentication | ||
|
|
||
| Here's a complete example using Clerk for authentication: | ||
|
|
||
| ```ts | ||
| import Hono from "hono"; | ||
| import { logger } from "hono/logger"; | ||
| // Hono with auth does not play nicely with @modelcontextprotocol/sdk yet, so we use the mcp-lite package | ||
| import { McpServer } from "mcp-lite"; | ||
| import { createClerkClient } from "@clerk/backend"; | ||
| import { | ||
| mcpAuthClerk, | ||
| protectedResourceHandlerClerk, | ||
| authServerMetadataHandlerClerk, | ||
| } from "@clerk/mcp-tools/hono"; | ||
|
|
||
| type AppType = { | ||
| Bindings: { | ||
| CLERK_SECRET_KEY: string; | ||
| CLERK_PUBLISHABLE_KEY: string; | ||
| } | ||
| }; | ||
|
|
||
| const app = new Hono<AppType>(); | ||
|
|
||
| const server = new McpServer({ | ||
| name: "clerk-mcp-server", | ||
| version: "1.0.0", | ||
| }); | ||
|
|
||
| server.tool( | ||
| "get_clerk_user_data", | ||
| { | ||
| description: "Gets data about the Clerk user that authorized this request" | ||
| handler: async (_, { authInfo, ...mcpContext }) => { | ||
| const clerkAuthInfo = authInfo; | ||
|
|
||
| const clerk = createClerkClient({ secretKey: mcpContext.state.CLERK_SECRET_KEY! }); | ||
|
|
||
| if (!clerkAuthInfo?.userId) { | ||
| return { | ||
| content: [{ type: "text", text: "Error: user not authenticated" }], | ||
| }; | ||
| } | ||
|
|
||
| const user = await clerk.users.getUser(clerkAuthInfo.userId); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(user) }], | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| app.use(logger()); | ||
|
|
||
| app.on( | ||
| ["GET", "OPTIONS"], | ||
| "/.well-known/oauth-protected-resource", | ||
| oauthCorsMiddleware, // <-- cors middleware is helpful for testing in the inspector | ||
| protectedResourceHandlerClerk() | ||
| ); | ||
| app.on( | ||
| ["GET", "OPTIONS"], | ||
| "/.well-known/oauth-protected-resource/mcp", | ||
| oauthCorsMiddleware, | ||
| protectedResourceHandlerClerk({ | ||
| scopes_supported: ["profile", "email"], | ||
| }) | ||
| ); | ||
| app.on( | ||
| ["GET", "OPTIONS"], | ||
| "/.well-known/oauth-authorization-server", | ||
| oauthCorsMiddleware, | ||
| authServerMetadataHandlerClerk | ||
| ); | ||
|
|
||
| app.post("/mcp", mcpAuthClerk, async (c) => { | ||
| const authInfo = c.get("auth"); | ||
| const transport = new StreamableHttpTransport(); | ||
| const mcpHttpHandler = transport.bind(server); | ||
| const response = await mcpHttpHandler(c.req.raw, { authInfo }); | ||
| return response; | ||
| }); | ||
|
|
||
| export default app; | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
| ## Authentication Middleware | ||
|
|
||
| ### `mcpAuthClerk` | ||
|
|
||
| Pre-configured authentication middleware for Clerk that automatically handles OAuth token verification. | ||
|
|
||
| **Example:** | ||
|
|
||
| ```ts | ||
| import { mcpAuthClerk } from "@clerk/mcp-tools/hono"; | ||
|
|
||
| // No additional configuration needed - uses Clerk's built-in token verification | ||
| app.post("/mcp", mcpAuthClerk, /** your mcp server handler */); | ||
| ``` | ||
|
|
||
| This middleware automatically: | ||
|
|
||
| - Verifies OAuth access tokens using Clerk | ||
| - Handles authentication state | ||
| - Adds Clerk auth data to request context via `c.get("auth")` | ||
|
|
||
| ## Protected Resource Metadata | ||
|
|
||
| ### `protectedResourceHandlerClerk` | ||
|
|
||
| Hono handler that returns OAuth protected resource metadata for Clerk integration, as required by [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). | ||
|
|
||
| **Example:** | ||
|
|
||
| ```ts | ||
| import { protectedResourceHandlerClerk } from "@clerk/mcp-tools/hono"; | ||
|
|
||
| app.get( | ||
| "/.well-known/oauth-protected-resource", | ||
| protectedResourceHandlerClerk({ scopes_supported: ["email"] }) | ||
| ); | ||
| ``` | ||
|
|
||
| ## Authorization Server Metadata | ||
|
|
||
| ### `authServerMetadataHandlerClerk` | ||
|
|
||
| Hono handler that returns OAuth authorization server metadata for Clerk integration, as defined by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414). This endpoint provides clients with information about Clerk's OAuth authorization server capabilities and endpoints. | ||
|
|
||
| **Example:** | ||
|
|
||
| ```ts | ||
| import { authServerMetadataHandlerClerk } from "@clerk/mcp-tools/hono"; | ||
|
|
||
| // Serve authorization server metadata at the standard well-known location | ||
| app.get( | ||
| "/.well-known/oauth-authorization-server", | ||
| authServerMetadataHandlerClerk | ||
| ); | ||
| ``` | ||
|
|
||
| **Note:** This handler requires the `CLERK_PUBLISHABLE_KEY` environment variable to be set, as it uses Clerk's public configuration to generate the metadata. | ||
|
|
||
|
|
||
| ## Accessing Authentication Data in Tools | ||
|
|
||
| Passing authentication data to your MCP tools is done via the `authInfo` parameter in the tool handler. | ||
|
|
||
| The `@modelcontextprotocol/sdk` package requires that you do this by monkeypatching an `Express.Request` object, however, so it does not play nicely with Hono. | ||
|
|
||
| The existing Hono MCP middleware does not yet support passing auth to MCP servers, but there is an open PR to add this support: https://github.com/honojs/middleware/pull/1318/files | ||
|
|
||
| Alternative libraries like [`mcp-lite`](https://github.com/fiberplane/mcp) (used in the example above) do support the `authInfo` parameter, provided you pass it to the MCP server HTTP handler. | ||
|
|
||
| ```typescript | ||
| import { McpServer } from "mcp-lite"; | ||
|
|
||
| const server = new McpServer({ | ||
| name: "my-server", | ||
| version: "1.0.0", | ||
| }); | ||
|
|
||
| server.tool( | ||
| "my-tool", | ||
| "My tool", | ||
| { type: "object", properties: {} }, | ||
| async (args, { authInfo }) => { | ||
| return { content: [{ type: "text", text: `Hello, ${authInfo.extra.userId}!` }] }; | ||
| } | ||
| ); | ||
|
|
||
| app.post("/mcp", mcpAuthClerk, async (c) => { | ||
| const authInfo = c.get("auth"); | ||
| const transport = new StreamableHttpTransport(); | ||
| const mcpHttpHandler = transport.bind(server); | ||
| // pass the authInfo to the MCP server HTTP handler, making it available to the tool handlers | ||
| const response = await mcpHttpHandler(c.req.raw, { authInfo }); | ||
| return response; | ||
| }); | ||
| ``` | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| When using Clerk integration, make sure to set: | ||
|
|
||
| ```bash | ||
| CLERK_PUBLISHABLE_KEY=pk_test_... | ||
| CLERK_SECRET_KEY=sk_test_... | ||
| ``` | ||
|
|
||
| The publishable key is used for generating OAuth metadata, while the secret key is used for server-side API calls to fetch user data. | ||
|
|
||
| ## Error Handling | ||
|
|
||
| The middleware automatically handles common authentication errors: | ||
|
|
||
| - **Missing Authorization header**: Returns 401 with `WWW-Authenticate` header pointing to your protected resource metadata | ||
| - **Invalid token format**: Throws an error with details about the expected format | ||
| - **Token verification failure**: Returns 401 with error details | ||
|
|
||
| ## Integration with Existing Hono Apps | ||
|
|
||
| These utilities are designed to integrate seamlessly with existing Hono applications. You can: | ||
|
|
||
| - Add MCP endpoints to existing routes | ||
| - Use your existing authentication middleware alongside MCP auth | ||
| - Combine with other Hono middleware (CORS, rate limiting, etc.) | ||
|
|
||
| ```ts | ||
| import cors from "cors"; | ||
| import { rateLimiter } from "hono-rate-limiter"; | ||
|
|
||
| // Apply middleware in the order you need | ||
| app.use(cors()); | ||
| app.use( | ||
| rateLimiter({ | ||
| windowMs: 15 * 60 * 1000, | ||
| limit: 100, | ||
| }) | ||
| ); | ||
|
|
||
| app.post( | ||
| "/mcp", | ||
| mcpAuthClerk, // MCP authentication | ||
| /** your mcp server handler */ | ||
| ); | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { createClerkClient } from "@clerk/backend"; | ||
| import { TokenType } from "@clerk/backend/internal"; | ||
| import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; | ||
| import { env } from "hono/adapter"; | ||
| import { createMiddleware } from "hono/factory"; | ||
| import { getPRMUrl } from "./utils.js"; | ||
| import { verifyClerkToken } from "../server"; | ||
|
|
||
| /** | ||
| * Hono middleware that enforces authentication for MCP requests using Clerk. | ||
| * | ||
| * Sets an "auth" variable on the request context, which matches the {@link AuthInfo} type from the MCP SDK. | ||
| */ | ||
| export const mcpAuthClerk = createMiddleware< | ||
| { Variables: { auth: AuthInfo } } | ||
| >(async (c, next) => { | ||
| const authHeader = c.req.header("Authorization"); | ||
| const [type, token] = authHeader?.split(" ") || []; | ||
| const bearerToken = type?.toLowerCase() === "bearer" ? token : undefined; | ||
|
|
||
| // Return 401 with proper www-authenticate header if no authorization is provided | ||
| if (!bearerToken) { | ||
| // Get the resource metadata url for the protected resource | ||
| // We return this in the `WWW-Authenticate` header so the MCP client knows where to find the protected resource metadata | ||
| const resourceMetadataUrl = getPRMUrl(c.req.raw); | ||
| c.header( | ||
| "WWW-Authenticate", | ||
| // NOTE - The mcp sdk also adds `error` and `error_description` to this header as well, depending on the error | ||
| // see: https://github.com/modelcontextprotocol/typescript-sdk/blob/b28c297184cb0cb64611a3357d6438dd1b0824c6/src/server/auth/middleware/bearerAuth.ts#L76C1-L95C8 | ||
| `Bearer resource_metadata="${resourceMetadataUrl}"`, | ||
| ); | ||
| return c.json({ error: "Unauthorized" }, 401); | ||
| } | ||
|
|
||
| try { | ||
| const secretKey = (env(c)?.CLERK_SECRET_KEY || "") as string; | ||
| const publishableKey = (env(c)?.CLERK_PUBLISHABLE_KEY || "") as string; | ||
|
|
||
| const clerkClient = createClerkClient({ | ||
| secretKey, | ||
| publishableKey, | ||
| }); | ||
|
|
||
| const requestState = await clerkClient.authenticateRequest(c.req.raw, { | ||
| secretKey, | ||
| publishableKey, | ||
| acceptsToken: TokenType.OAuthToken, | ||
| }); | ||
|
|
||
| // This is the result of the authenticateRequest call, with the `TokenType.OAuthToken` type | ||
| const auth = requestState.toAuth(); | ||
|
|
||
| const authInfo = verifyClerkToken(auth, token); | ||
|
|
||
| // Require valid auth for this endpoint | ||
| if (!authInfo) { | ||
| return c.json({ error: "Unauthorized" }, 401); | ||
| } | ||
|
|
||
| // Attach auth to Request and Hono context for downstream handlers | ||
| c.set("auth", authInfo); | ||
|
|
||
| await next(); | ||
| } catch (error) { | ||
| console.error("Unexpected mcp auth middleware error:", error); | ||
| return c.json({ error: "Internal Server Error" }, 500); | ||
| } | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export { mcpAuthClerk } from "./auth.js"; | ||
| export { | ||
| oauthCorsMiddleware, | ||
| protectedResourceHandlerClerk, | ||
| authServerMetadataHandlerClerk, | ||
| } from "./oauth-server-routes.js"; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.