Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ For detailed implementation guides and examples specific to your framework, see:

- **[Express.js Integration](./express/README.md)** - Complete guide for building MCP servers with Express.js
- **[Next.js Integration](./next/README.md)** - Complete guide for building both MCP servers and clients with Next.js
- **[Hono Integration](./hono/README.md)** - Complete guide for building MCP servers with Hono

### Table of Contents

Expand Down
251 changes: 251 additions & 0 deletions hono/README.md
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 */
);
```
68 changes: 68 additions & 0 deletions hono/auth.ts
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);
}
});
6 changes: 6 additions & 0 deletions hono/index.ts
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";
Loading