billet-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
billet-mcp
A remote MCP endpoint with an OAuth 2.1 sign-in, for Billet apps. Claude connectors (and ChatGPT) can register themselves, send a person through a consent screen, and then call the app's tools as that person.
It owns the generic surface and nothing else:
OAuth 2.1 server: dynamic client registration (RFC 7591), authorize with PKCE (S256 only), authorization codes, rotating refresh tokens, revocation of a grant.
Discovery: RFC 9728 protected-resource metadata and RFC 8414 authorization-server metadata.
The consent screen.
A bearer gate whose 401 carries the RFC 9728
resource_metadatapointer.A stateless
/mcpendpoint and tool-call auditing.
The app supplies the database, its URL, a logger, its users, its sign-in page and its tools. The package imports nothing Billet-specific and keeps its data in its own mcp_ tables.
Install
bun add @alexpricedev/billet-mcpRequires Bun (it uses Bun.SQL) and Postgres.
Related MCP server: mcp-app-server
Add the migration
The package ships up and down functions. Wrap them in one migration file of your app's:
// migrations/0xx_add_mcp.ts
import { down as mcpDown, up as mcpUp } from "@alexpricedev/billet-mcp";
import type { SQL } from "bun";
export const up = (db: SQL) => mcpUp(db);
export const down = (db: SQL) => mcpDown(db);It creates mcp_clients, mcp_grants, mcp_tokens, mcp_auth_requests and mcp_audit. Every constraint and index is named with the mcp_ prefix. identity_id is TEXT with no foreign key, so your user ids can be UUIDs, integers or anything else stable.
Mount the routes
import { createMcp, MCP_CONTINUE_PATH } from "@alexpricedev/billet-mcp";
export const mcp = createMcp({
sql: db, // a Bun SQL instance
appUrl: process.env.APP_URL!, // e.g. https://app.example.com
log, // { warn, error, info? }(scope, message)
server: {
name: "My app",
version: "1.0.0",
instructions: "What the model should know about this server.",
},
getUser: async (req) => { // the signed-in user, or null
const session = await sessionFrom(req);
return session ? { id: session.user.id, email: session.user.email } : null;
},
findUser: async (id) => lookupUser(id), // fresh read by id, or null
signInUrl: "/sign-in", // string, or (req) => string
registerTools: (server, user) => {
server.registerTool("whoami", { description: "Who am I?" }, async () => ({
content: [{ type: "text", text: user.email }],
}));
},
});
Bun.serve({
routes: {
...mcp.routes,
// ...your own routes
},
});mcp.routes is a plain object of (req: Request) => Response | Promise<Response>:
Route | What |
| the stateless MCP endpoint (bearer token) |
| RFC 9728 metadata |
| RFC 8414 metadata |
| dynamic client registration |
| the consent screen (GET) and its decision (POST) |
| where sign-in returns to |
| code exchange and refresh |
The discovery documents must be reachable at the root of your origin, so mount them there.
The seams
Option | Purpose |
| Bun |
| Your public URL. Only its origin is used: it is the OAuth issuer and |
|
|
| Name, version and instructions the MCP server reports. |
| The signed-in user for a browser request: |
| The user behind a stored grant, or |
| Where a signed-out browser goes to sign in. |
| Register your tools on the per-request |
| Recommended: registration and authorize are public and write rows, so without it they are unlimited. Return a |
| Wraps the two HTML routes so your CSP and friends apply. |
| Replaces the package's plain page chrome. |
| Tool argument names kept in the audit row (default |
The package's own page uses an inline <style>. If your CSP forbids that, pass renderPage.
Sign-in and coming back
A signed-out visit to /mcp/authorize stores the pending request, sets the HttpOnly mcp_auth cookie (Path=/, ten minutes) and redirects to signInUrl. After your sign-in completes, send the browser on to MCP_CONTINUE_PATH (/mcp/authorize/continue) when a request is pending (hasPendingAuthorize is false once it has expired):
if (await mcp.hasPendingAuthorize(req)) return Response.redirect(MCP_CONTINUE_PATH);/mcp/authorize/continue shows the consent screen, or redirects to / when there is no pending request.
Connect Claude
Deploy the app with the routes mounted and
appUrlset to its public URL.In Claude, add a custom connector and enter
https://your-app.example.com/mcp.Claude gets a 401 with
resource_metadata, discovers the server, registers itself, and opens/mcp/authorizein your browser.Sign in if you are not, then choose Allow access on the consent screen.
For Claude Code: claude mcp add --transport http my-app https://your-app.example.com/mcp, then /mcp inside Claude Code to sign in.
Behaviour
One scope,
content. Anything else is refused, not narrowed.Public clients only (
token_endpoint_auth_method: none). Registration accepts only Claude's and ChatGPT's callbacks and loopback redirects (http://localhost,127.0.0.1,[::1]).Lifetimes: authorization code 60 seconds, access token 1 hour, refresh token 30 days, pending authorize request 10 minutes.
Codes are single use. Refresh tokens rotate, and presenting a spent one revokes the whole grant.
Tokens are 256 random bits, stored only as a SHA-256. An access token is also checked against its grant, its audience (
<origin>/mcp) andfindUser.The consent form is protected by a double-submit token in an HttpOnly cookie plus an
Origincheck.JSON-RPC batches are refused, so the rate limit and the audit see every call.
There is no development token: OAuth is the only way in.
Every tool call writes one
mcp_auditrow (tool, grant, outcome, target arguments), and so do grant approvals and revocations.
Also exported from createMcp: revokeGrant(grantId, actorId), listGrants() for a connected-apps page, and cleanupExpired() to delete spent credentials (run it now and then).
Development
bun install
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/billet_mcp_test bun run check
bun run buildThe tests run the OAuth flow against Postgres and drop and recreate the mcp_ tables, so point DATABASE_URL at a throwaway database.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Your OpenWork org's skills, plugins, workflows, and connections through one OAuth MCP URL.
One OAuth connector for your team's remote MCP servers, with Tool Packs, guardrails and logs.
Connect any AI agent to 1,000+ apps and 27,000+ actions through one remote MCP server (OAuth).
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables deploying a remote MCP server with OAuth 2.1 resource-server authorization, protecting tools behind validated bearer tokens and RFC 9728 discovery.MIT
- AlicenseNot gradedqualityBmaintenanceEnables existing apps to expose their users and data to external AI agents via MCP with OAuth 2.1 authentication and scoped tools.46 npmMIT
- AlicenseNot gradedqualityBmaintenanceProvides an OAuth 2.1 authorization and login layer plus tool registration for MCP connectors, so a connector can be added to Claude clients with only a URL and password. It also shapes tool names, descriptions and input schemas for what Claude clients actually display, returning compact results and readable error messages.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients to securely connect to your data through a remote deployment with OAuth 2.1 and PKCE, configurable sign-in, read/write consent, and audit logging. It exposes public tools without sign-in and authenticated tools that respect each user's permissions while validating arguments and recording every call.Apache 2.0