├── .cursor └── mcp.json ├── .env.template ├── .gitignore ├── LICENSE ├── README.md ├── bun.lock ├── docker-compose.yml ├── index.ts ├── package.json ├── scripts └── jwt.ts ├── src ├── app.stateful.ts ├── app.stateless.ts ├── lib │ ├── errors.ts │ ├── extended-oauth-proxy-provider.ts │ ├── storage │ │ ├── in-memory.ts │ │ └── redis.ts │ └── types.ts └── mcp-server.ts └── tsconfig.json /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "streamable": { 4 | "command": "bunx", 5 | "args": [ 6 | "mcp-remote", 7 | "http://localhost:5050/mcp", 8 | "--transport", 9 | "http-first" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | OAUTH_CLIENT_ID= 2 | OAUTH_CLIENT_SECRET= 3 | OAUTH_REGISTRATION_URL=https:///oidc/register 4 | OAUTH_ISSUER_URL=https:// 5 | OAUTH_AUTHORIZATION_URL=https:///authorize 6 | OAUTH_TOKEN_URL=https:///oauth/token 7 | #OAUTH_REVOCATION_URL= 8 | 9 | # the host & port the server is deployed to; used for calback URL; used for registering callback URL to Auth0 10 | THIS_HOSTNAME="http://localhost:5050" 11 | LOG_LEVEL=debug 12 | DEBUG=index.ts 13 | TOKEN_STORAGE_STRATEGY='memory' # 'memory' for in-memory stroage; use 'redis' please. 14 | REDIS_DSN=redis://127.0.0.1:6379 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | *.env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Naptha AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌊 HTTP + SSE MCP Server w/ OAuth 2 | 3 | ## Introduction 4 | This repo provides a reference implementation for creating a remote MCP server that supports the Streamable HTTP & SSE Transports, authorized with OAuth based on the MCP specification. 5 | 6 | Note that the MCP server in this repo is logically separate from the application that handles the report SSE + HTTP transports, and from OAuth. 7 | 8 | As a result, you can easily fork this repo, and plug in your own MCP server and OAuth credentials for a working SSE/HTTP + OAuth MCP server with your own functionality. 9 | 10 | > **But, why?** 11 | 12 | Great question! The MCP specification added the authorization specification based on OAuth on March 25, 2025. At present, as of May 1, 2025: 13 | - The Typescript SDK contains many of the building blocks for accomplishing an OAuth-authorized MCP server with streamable HTTP, **but there is no documentation or tutorial** on how to build such a server 14 | - The Python SDK contains neither an implementation of the streamable HTTP transport, nor an implementation of the OAuth building blocks that are present in the typescript SDK 15 | - The Streamable HTTP transport is broadly unsupported by MCP host applications such as Cursor and Claude desktop, though it may be integrated directly into agents written in JavaScript using the JS/TS SDK's `StreamableHttpClientTransport` class 16 | 17 | At [Naptha AI](https://naptha.ai), we really wanted to build an OAuth-authorized MCP server on the streamable HTTP transport, and couldn't find any reference implementations, so we decided to build one ourselves! 18 | 19 | 20 | ## Dependencies 21 | [Bun](https://bun.sh), a fast all-in-one JavaScript runtime, is the recommended runtime and package manager for this repository. Limited compatibility testing has been done with `npm` + `tsc`. 22 | 23 | 24 | 25 | ## Overview 26 | This repository provides the following: 27 | 1. An MCP server, which you can easily replace with your own 28 | 2. An express.js application that manages _both_ the SSE and Streamable HTTP transports _and_ OAuth authorization. 29 | 30 | This express application is what you plug your credentials and MCP server into. 31 | 32 | Note that while this express app implements the required OAuth endpoints including `/authorize` and the Authorization Server Metadata endpoint ([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414)), _it does not implement an OAuth authorization server!_ 33 | 34 | This example proxies OAuth to an upstream OAuth server which supports dynamic client registration ([RFC7591](https://datatracker.ietf.org/doc/html/rfc7591)). To use this example, you will need to bring your own authorization server. We recommend using [Auth0](https://auth0.com); see the ["Setting up OAuth" Section](https://github.com/NapthaAI/http-oauth-mcp-server?tab=readme-ov-file#setting-up-oauth) below. 35 | 36 | 37 | ## Configuring your server 38 | ### Notes on OAuth & Dynamic Client Registration 39 | To use this example, you need an OAuth authorization server. _Do not implement this yourself!_ For the purposes of creating our demo, we used [Auth0](https://auth0.com) -- this is a great option, though there are many others. 40 | 41 | The MCP specification requires support for an uncommon OAuth feature, specifically [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591), Dynamic Client Registration. The [MCP specification](https://modelcontextprotocol.io/specification/2025-03-2026) specifies that MCP clients and servers should support the Dynamic client registration protocol, so that MCP clients (whever your client transport lives) can obtain Client IDs without user registration. This allows new clients (agents, apps, etc.) to automatically register with new servers. More details on this can be found [in the authorization section of the MCP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-4-dynamic-client-registration), but this means that unfortunately, you cannot simply proxy directly to a provider like Google or GitHub, which do not support dynamic client registration (they require you to register clients in their UI). 42 | 43 | This leaves you with two options: 44 | 1. Pick an upstream OAuth provider like Auth0 which allows you to use OIDC IDPs like Google and GitHub for authentication, and which _does_ support dynamic client registration, or 45 | 2. implement dynamic client registration in the application yourself (i.e., the express application becomes not just a simple OAuth proxy but a complete or partially-complete OAuth server). Cloudflare implemented something like this for their Workers OAuth MCP servers, which we may extend this project with later. You can find that [here](https://github.com/cloudflare/workers-oauth-provider). 46 | 47 | For simplicity, we have opted for the former option using Auth0. 48 | 49 | > [!NOTE] 50 | > Since this implementation proxies the upstream OAuth server, the default approach of forwarding the access token from the OAuth server to the client would expose the user's upstream access token to the downstream client & MCP host. This is not suitable for many use-cases, so this approach re-implements some `@modelcontextprotocol/typescript-sdk` classes to fix this issue. 51 | 52 | Note that while we are proxying the upstream authorization server, we are _not_ returning the end-user's auth token to the MCP client / host - instead, we are issuing our own, and allowing the client / host to use that token to authorize with our server. This prevents a malicious client or host from abusing the token, or from it being abused if it's leaked. 53 | 54 | 55 | ### Setting up OAuth with Auth0 56 | To get started with Auth0: 57 | 1. Create an Auth0 account at [Auth0.com](https://auth0.com/). 58 | 2. Create at least one connection to an IDP such as Google or GitHub. You can [learn how to do this here](https://auth0.com/docs/authenticate/identity-providers). 59 | 3. Promote the connection to a _domain-level connection_. Since new OAuth clients are registered by each MCP client, you can't configure your IDP connections on a per-application/client basis. This means your connections need to be available for all apps in your domain. You can [learn how do this here](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level). 60 | 4. Enable Dynamic Client Registration (auth0 also calls this "Dynamic Application Registration"). You can [learn how to do this here](https://auth0.com/docs/get-started/applications/dynamic-client-registration). 61 | 62 | Once all of this has been set up, you will need the following information: 63 | * your Auth0 client ID 64 | * your Auth0 client secret 65 | * your Auth0 tenant domain 66 | 67 | Make sure to fill this information into your `.env`. Copy `.env.template` and then update the values with your configurations & secrets. 68 | 69 | 70 | 71 | ## Running the server 72 | This repository includes two separate stand-alone servers: 73 | - a **stateless** implementation of the streamable HTTP server at `src/app.stateless.ts`. This only supports the streamable HTTP transport, and is (theoretically) suitable for serverless deployment 74 | - a **stateful** implementation of both SSE and streamable HTTP at `src/app.stateful.ts`. This app offers both transports, but maintains in-memory state even when using the `redis` storage strategy (connections must be persisted in-memory), so it is not suitable for serverless deployment or trivial horizontal scaling. 75 | 76 | You can run either of them with `bun`: 77 | 78 | ```shell 79 | bun run src/app.stateless.ts 80 | # or, 81 | bun run src/app.stateful.ts 82 | ``` 83 | 84 | ## Putting it All Together 85 | To test out our MCP server with streamable HTTP and OAuth support, you have a couple options. 86 | 87 | As noted above, the Python MCP SDK does not support these features, so currently you can either plug our remote server into an MCP host like Cursor or Claude Desktop, or into a TypeScript/JavaScript application directly - but not into a Python one. 88 | 89 | ### Plugging your server into your MCP Host (Cursor / Claude) 90 | Since most MCP hosts don't support either streamable HTTP (which is superior to SSE in a number of ways) _or_ OAuth, we recommend using the `mcp-remote` npm package which will handle the OAuth authorization, and bridging the remote transport into a STDIO transport for your host. 91 | 92 | the command will look like this: 93 | 94 | ```shell 95 | bunx mcp-remote --transport http-first https://some-domain.server.com/mcp 96 | # or, 97 | npx mcp-remote --transport http-first https://some-domain.server.com/mcp 98 | ``` 99 | 100 | You have a couple of options for the `--transport` option: 101 | - `http-first` (default): Tries HTTP transport first, falls back to SSE if HTTP fails with a 404 error 102 | - `sse-first`: Tries SSE transport first, falls back to HTTP if SSE fails with a 405 error 103 | - `http-only`: Only uses HTTP transport, fails if the server doesn't support it 104 | - `sse-only`: Only uses SSE transport, fails if the server doesn't support it 105 | 106 | > [!NOTE] 107 | > If you launch the _stateless_ version of the server with `src/app.stateless.ts`, the SSE transport is not available, so you should use `--transport http-only`. SSE transport should not be expected to work if you use this entrypoint. 108 | 109 | 110 | ### Plugging you server into your agent 111 | You can plug your Streamable HTTP server into an agent in JS/TS using `StreamableHTTPClientTransport`. However, this will not work with OAuth-protected servers. Instead, you should use the `Authorization` header on the client side, with a valid access token on the server side. 112 | 113 | You can implement this with client credentials, API keys or something else. That pattern is not supported in this repository, but it would look like this using the [Vercel AI SDK](https://ai-sdk.dev/cookbook/node/mcp-tools#mcp-tools): 114 | 115 | ```typescript 116 | import { openai } from '@ai-sdk/openai'; 117 | import { experimental_createMCPClient as createMcpClient, generateText } from 'ai'; 118 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 119 | 120 | const mcpClient = await createMcpClient({ 121 | transport: new StreamableHTTPClientTransport( 122 | new URL("http://localhost:5050/mcp"), { 123 | requestInit: { 124 | headers: { 125 | Authorization: "Bearer YOUR TOKEN HERE", 126 | }, 127 | }, 128 | // TODO add OAuth client provider if you want 129 | authProvider: undefined, 130 | }), 131 | }); 132 | 133 | const tools = await mcpClient.tools(); 134 | await generateText({ 135 | model: openai("gpt-4o"), 136 | prompt: "Hello, world!", 137 | tools: { 138 | ...(await mcpClient.tools()) 139 | } 140 | }); 141 | 142 | ``` 143 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "http-oauth-mcp-server", 6 | "dependencies": { 7 | "@modelcontextprotocol/sdk": "^1.11.0", 8 | "dotenv": "^16.5.0", 9 | "express": "^5.1.0", 10 | "ioredis": "^5.6.1", 11 | "jsonwebtoken": "^9.0.2", 12 | "jwks-rsa": "^3.2.0", 13 | "logging": "^3.3.0", 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "latest", 17 | "@types/jsonwebtoken": "^9.0.9", 18 | }, 19 | "peerDependencies": { 20 | "typescript": "^5", 21 | }, 22 | }, 23 | }, 24 | "packages": { 25 | "@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="], 26 | 27 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.11.0", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ=="], 28 | 29 | "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], 30 | 31 | "@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="], 32 | 33 | "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], 34 | 35 | "@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], 36 | 37 | "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], 38 | 39 | "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], 40 | 41 | "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="], 42 | 43 | "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], 44 | 45 | "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 46 | 47 | "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="], 48 | 49 | "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], 50 | 51 | "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], 52 | 53 | "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], 54 | 55 | "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], 56 | 57 | "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 58 | 59 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 60 | 61 | "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], 62 | 63 | "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], 64 | 65 | "bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="], 66 | 67 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 68 | 69 | "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 70 | 71 | "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 72 | 73 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 74 | 75 | "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], 76 | 77 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 78 | 79 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 80 | 81 | "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], 82 | 83 | "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 84 | 85 | "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 86 | 87 | "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], 88 | 89 | "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 90 | 91 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 92 | 93 | "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 94 | 95 | "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], 96 | 97 | "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 98 | 99 | "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], 100 | 101 | "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 102 | 103 | "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], 104 | 105 | "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 106 | 107 | "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 108 | 109 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 110 | 111 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 112 | 113 | "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 114 | 115 | "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 116 | 117 | "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 118 | 119 | "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 120 | 121 | "eventsource": ["eventsource@3.0.6", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA=="], 122 | 123 | "eventsource-parser": ["eventsource-parser@3.0.1", "", {}, "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA=="], 124 | 125 | "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], 126 | 127 | "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], 128 | 129 | "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], 130 | 131 | "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 132 | 133 | "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], 134 | 135 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 136 | 137 | "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 138 | 139 | "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 140 | 141 | "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 142 | 143 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 144 | 145 | "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 146 | 147 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 148 | 149 | "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 150 | 151 | "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 152 | 153 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 154 | 155 | "ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="], 156 | 157 | "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 158 | 159 | "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], 160 | 161 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 162 | 163 | "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], 164 | 165 | "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], 166 | 167 | "jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], 168 | 169 | "jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="], 170 | 171 | "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], 172 | 173 | "limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="], 174 | 175 | "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], 176 | 177 | "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], 178 | 179 | "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], 180 | 181 | "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], 182 | 183 | "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], 184 | 185 | "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], 186 | 187 | "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], 188 | 189 | "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], 190 | 191 | "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], 192 | 193 | "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], 194 | 195 | "logging": ["logging@3.3.0", "", { "dependencies": { "chalk": "^4.1.0", "debug": "^4.3.1", "nicely-format": "^1.1.0" } }, "sha512-Hnmu3KlGTbXMVS7ONjBpnjjiF9cBlK5qsmj77sOcqRkNpvO9ouUGPKe2PmBCWWYpKAbxb96b08cYEv4hiBk3lQ=="], 196 | 197 | "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], 198 | 199 | "lru-memoizer": ["lru-memoizer@2.3.0", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" } }, "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug=="], 200 | 201 | "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 202 | 203 | "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 204 | 205 | "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 206 | 207 | "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 208 | 209 | "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], 210 | 211 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 212 | 213 | "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], 214 | 215 | "nicely-format": ["nicely-format@1.1.0", "", { "dependencies": { "ansi-styles": "^2.2.1", "esutils": "^2.0.2" } }, "sha512-nZk4ea8ZeH6KKpfzhopC7sC0KeN2+QZQIxp2jvElndkyXuM3pJqB8I4fTypkWCyeKBJRLdQ1h/O2L48lS6S6gg=="], 216 | 217 | "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 218 | 219 | "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 220 | 221 | "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 222 | 223 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 224 | 225 | "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 226 | 227 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 228 | 229 | "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], 230 | 231 | "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], 232 | 233 | "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 234 | 235 | "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 236 | 237 | "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 238 | 239 | "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], 240 | 241 | "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], 242 | 243 | "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], 244 | 245 | "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], 246 | 247 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 248 | 249 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 250 | 251 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 252 | 253 | "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], 254 | 255 | "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], 256 | 257 | "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 258 | 259 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 260 | 261 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 262 | 263 | "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 264 | 265 | "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 266 | 267 | "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 268 | 269 | "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 270 | 271 | "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], 272 | 273 | "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 274 | 275 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 276 | 277 | "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 278 | 279 | "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 280 | 281 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 282 | 283 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 284 | 285 | "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 286 | 287 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 288 | 289 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 290 | 291 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 292 | 293 | "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 294 | 295 | "zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="], 296 | 297 | "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], 298 | 299 | "nicely-format/ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | valkey: 3 | image: valkey/valkey:latest 4 | ports: 5 | - 127.0.0.1:6379:6379 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-oauth-mcp-server", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | "@types/jsonwebtoken": "^9.0.9" 9 | }, 10 | "peerDependencies": { 11 | "typescript": "^5" 12 | }, 13 | "dependencies": { 14 | "@modelcontextprotocol/sdk": "^1.11.0", 15 | "dotenv": "^16.5.0", 16 | "express": "^5.1.0", 17 | "ioredis": "^5.6.1", 18 | "jsonwebtoken": "^9.0.2", 19 | "jwks-rsa": "^3.2.0", 20 | "logging": "^3.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import jwksClient from "jwks-rsa"; 3 | 4 | const client = jwksClient({ 5 | jwksUri: "https://naptha.jp.auth0.com/.well-known/jwks.json", 6 | timeout: 30_000, // 30 seconds 7 | }); 8 | 9 | const jwtToken = 10 | "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InoxVmx1eHV5Wk5IUHNvbXA0WkxEVCJ9.eyJuaWNrbmFtZSI6IkstTWlzdGVsZSIsIm5hbWUiOiJLeWxlIE1pc3RlbGUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzE4NDMwNTU1P3Y9NCIsInVwZGF0ZWRfYXQiOiIyMDI1LTA1LTAyVDAzOjAzOjIyLjAyOVoiLCJlbWFpbCI6Imt5bGVAbWlzdGVsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9uYXB0aGEuanAuYXV0aDAuY29tLyIsImF1ZCI6IndsYkp4R25CTjRUZ015SXh4MVNwRWQySlJ0TDlmY3FwIiwic3ViIjoiZ2l0aHVifDE4NDMwNTU1IiwiaWF0IjoxNzQ2MjA2NTczLCJleHAiOjE3NDYyNDI1NzMsInNpZCI6Iks1ZmpLWEJnU1hZV1QtMW85a0RscmxERTE3WnhxT2EzIn0.MBxl6VuFZKzwfGBRep4aWyYKu1f6kY0ZT15yC2Ba66gQVzmVWF88aA4IgLwuGkakLACTlHXu49N_lZykl-1JuuDl62d5eWkJDD642D_5iiYMVuK_0ac50ZXpQOiX25PBfXwDR1a4FE7YaL87fLhUaQtF8WXBHiSXMzKkAI-JjAQzC7EuHbtXVK-NP3aW4QncLEEqKhhSulB1oTGX4Y1lvm0kJTutPsvIVjPmkU2m95UxFEg9e0xL1E87rMMX35BvTwzgbEbRNjttzGN_W9fBqJSqYGFctt-0GcH8op2n5EwfnYoQo4iEI90CtYPa3AfMhb7MwBQnsUSS0hCX4pSzPw"; 11 | 12 | const tokenInfo = jwt.decode(jwtToken, { complete: true }); 13 | console.log(tokenInfo); 14 | const key = await client.getSigningKey(tokenInfo?.header.kid); 15 | const signingKey = key.getPublicKey(); 16 | 17 | const verified = jwt.verify(jwtToken, signingKey); 18 | console.log("verified info:", verified); 19 | -------------------------------------------------------------------------------- /src/app.stateful.ts: -------------------------------------------------------------------------------- 1 | import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"; 2 | import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js"; 3 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 5 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; 6 | import { config } from "dotenv"; 7 | import express, { 8 | type NextFunction, 9 | type Request, 10 | type Response, 11 | } from "express"; 12 | import createLogger from "logging"; 13 | import { randomUUID } from "node:crypto"; 14 | import { InvalidAccessTokenError } from "./lib/errors"; 15 | import { ExtendedProxyOAuthServerProvider } from "./lib/extended-oauth-proxy-provider"; 16 | import InMemoryStorage from "./lib/storage/in-memory"; 17 | import { RedisStorage } from "./lib/storage/redis"; 18 | import type { OAuthProxyStorageManager } from "./lib/types"; 19 | import { server } from "./mcp-server"; 20 | config(); 21 | 22 | const logger = createLogger(__filename.split("/").pop() ?? "", { 23 | debugFunction: (...args) => { 24 | console.log(...args); 25 | }, 26 | }); 27 | const { 28 | OAUTH_ISSUER_URL, 29 | OAUTH_AUTHORIZATION_URL, 30 | OAUTH_TOKEN_URL, 31 | OAUTH_REVOCATION_URL, 32 | OAUTH_REGISTRATION_URL, 33 | THIS_HOSTNAME, 34 | } = process.env; 35 | 36 | if ( 37 | !OAUTH_ISSUER_URL || 38 | !OAUTH_AUTHORIZATION_URL || 39 | !OAUTH_TOKEN_URL || 40 | !OAUTH_REGISTRATION_URL || 41 | !THIS_HOSTNAME 42 | ) { 43 | throw new Error("Missing environment variables"); 44 | } 45 | 46 | // NOTE ideally we don't do this in memory since it's not horizontally scalable easily 47 | // but these are stateful objects with connections from the client so they can't just 48 | // be written to a database. 49 | const transports: { 50 | sse: { [sessionId: string]: SSEServerTransport }; 51 | streamable: { [sessionId: string]: StreamableHTTPServerTransport }; 52 | } = { 53 | sse: {}, 54 | streamable: {}, 55 | }; 56 | 57 | let storageStrategy: OAuthProxyStorageManager; 58 | if (process.env.TOKEN_STORAGE_STRATEGY === "redis") { 59 | logger.info("Using redis storage strategy!"); 60 | storageStrategy = RedisStorage; 61 | } else { 62 | logger.warn( 63 | "Using in-memory storage strategy. DO NOT USE THIS IN PRODUCTION!", 64 | ); 65 | storageStrategy = InMemoryStorage; 66 | } 67 | 68 | process.env.TOKEN_STORAGE_STRATEGY === "memory" 69 | ? InMemoryStorage 70 | : RedisStorage; 71 | 72 | const app = express(); 73 | app.use(express.json()); 74 | app.use(express.urlencoded({ extended: false })); 75 | 76 | // Set up the OAuth Proxy provider; configured in .env to use Naptha's Auth0 tenant 77 | const proxyProvider = new ExtendedProxyOAuthServerProvider({ 78 | endpoints: { 79 | authorizationUrl: `${OAUTH_AUTHORIZATION_URL}`, 80 | tokenUrl: `${OAUTH_TOKEN_URL}`, 81 | revocationUrl: OAUTH_REVOCATION_URL, 82 | registrationUrl: `${OAUTH_REGISTRATION_URL}`, 83 | }, 84 | 85 | storageManager: storageStrategy, // configure with process.env.TOKEN_STORAGE_STRATEGY 86 | }); 87 | 88 | // Set up the middleware that verifies the issued bearer tokens. Note that these are NOT 89 | // the auth tokens from the upstream IDP. 90 | const bearerAuthMiddleware = requireBearerAuth({ 91 | provider: proxyProvider, 92 | requiredScopes: [], 93 | }); 94 | 95 | // Mount the router that handles the OAuth Proxy's endoints, discovery etc. 96 | app.use( 97 | mcpAuthRouter({ 98 | provider: proxyProvider, 99 | issuerUrl: new URL(`${OAUTH_ISSUER_URL}`), // address of issuer, auth0 100 | baseUrl: new URL(`${THIS_HOSTNAME}`), // address of local server 101 | }), 102 | ); 103 | 104 | /** 105 | * Set up the SSE MCP router 106 | */ 107 | app.get("/sse", bearerAuthMiddleware, async (req, res) => { 108 | logger.debug("SSE headers:", req.headers); 109 | logger.debug("SSE body:", req.body); 110 | 111 | const transport = new SSEServerTransport("/messages", res); 112 | transports.sse[transport.sessionId] = transport; 113 | 114 | res.setTimeout(1_000 * 60 * 60 * 6); // 6 hours 115 | 116 | res.on("close", () => { 117 | delete transports.sse[transport.sessionId]; 118 | }); 119 | 120 | await server.connect(transport); 121 | }); 122 | 123 | // Legacy message endpoint for older clients 124 | app.post("/messages", bearerAuthMiddleware, async (req, res) => { 125 | const sessionId = req.query.sessionId as string; 126 | logger.debug("SSE", sessionId, "Received message"); 127 | const transport = transports.sse[sessionId]; 128 | if (transport) { 129 | logger.debug("SSE", sessionId, "Transport found for sessionId"); 130 | await transport.handlePostMessage(req, res, req.body); 131 | logger.debug( 132 | "SSE", 133 | sessionId, 134 | "Message handled by transport for sessionId", 135 | ); 136 | } else { 137 | logger.warn("SSE", sessionId, "No transport found for sessionId"); 138 | res.status(400).send("No transport found for sessionId"); 139 | } 140 | }); 141 | 142 | /** 143 | * Set up the streamable HTTP MCP router 144 | */ 145 | app.use("/", async (req, res, next) => { 146 | logger.debug(req.method, req.url, req.headers, req.body); 147 | await next(); 148 | logger.debug(res.headersSent, res.statusCode); 149 | }); 150 | app.post("/mcp", bearerAuthMiddleware, async (req, res, next) => { 151 | const sessionId = req.headers["mcp-session-id"] as string | undefined; 152 | logger.info("Streamable", "Received message for session", sessionId); 153 | logger.debug(req.body); 154 | logger.debug( 155 | "Streamable", 156 | "is initialize request?", 157 | isInitializeRequest(req.body), 158 | ); 159 | let transport: StreamableHTTPServerTransport; 160 | 161 | // If the sessionID is set and it's associated with a transport, use it 162 | if (sessionId && transports.streamable[sessionId]) { 163 | transport = transports.streamable[sessionId]; 164 | logger.info("Streamable", "Transport found for sessionId", sessionId); 165 | 166 | // if the session id IS NOT available and it's an initialize request, set up a new one 167 | } else if (!sessionId && isInitializeRequest(req.body)) { 168 | logger.info("Streamable", "Setting up a new transport"); 169 | // Create a new transport with a UUID as sesssion ID; saving it to the transports object 170 | transport = new StreamableHTTPServerTransport({ 171 | sessionIdGenerator: randomUUID, 172 | onsessioninitialized(sessionId) { 173 | transports.streamable[sessionId] = transport; 174 | }, 175 | }); 176 | 177 | transport.onclose = () => { 178 | if (transport.sessionId) 179 | delete transports.streamable[transport.sessionId]; 180 | }; 181 | logger.info("Streamable", transport.sessionId, "Transport constructed"); 182 | 183 | // connect to the new server 184 | await server.connect(transport); 185 | logger.info( 186 | "Streamable", 187 | transport.sessionId, 188 | "Server connected to transport", 189 | ); 190 | } else { 191 | logger.warn("Streamable", sessionId, "No transport found for sessionId"); 192 | res.status(400).json({ 193 | jsonrpc: "2.0", 194 | error: { 195 | code: -32_000, 196 | message: "No transport found for sessionId", 197 | }, 198 | id: null, 199 | }); 200 | return next(); 201 | } 202 | 203 | await transport.handleRequest(req, res, req.body); 204 | logger.info( 205 | "Streamable", 206 | "Message handled by transport for session", 207 | sessionId, 208 | ); 209 | }); 210 | 211 | // Reusable handler for GET and delete requests 212 | 213 | const handleSessionRequest = async ( 214 | req: Request, 215 | res: Response, 216 | next: NextFunction, 217 | ) => { 218 | const sessionId = req.headers["mcp-session-id"] as string | undefined; 219 | if (!sessionId || !transports.streamable[sessionId]) { 220 | logger.warn("Streamable", sessionId, "No transport found for sessionId"); 221 | res.status(400).json({ 222 | jsonrpc: "2.0", 223 | error: { 224 | code: -32_000, 225 | message: "No transport found for sessionId", 226 | }, 227 | id: null, 228 | }); 229 | return next(); 230 | } 231 | const transport = transports.streamable[sessionId]; 232 | await transport.handleRequest(req, res); 233 | }; 234 | 235 | app.get("/mcp", handleSessionRequest); 236 | app.delete("/mcp", handleSessionRequest); 237 | app.use((error: Error, req: Request, res: Response, next: NextFunction) => { 238 | logger.info("Error", error); 239 | if (!res.headersSent) { 240 | if (error instanceof InvalidAccessTokenError) { 241 | res.status(401).json({ 242 | jsonrpc: "2.0", 243 | error: { 244 | code: -32_000, 245 | message: "Invalid access token", 246 | }, 247 | id: null, 248 | }); 249 | } else { 250 | res.status(500).json({ 251 | jsonrpc: "2.0", 252 | error: { 253 | code: -32_000, 254 | message: "Internal server error", 255 | }, 256 | id: null, 257 | }); 258 | } 259 | } else { 260 | logger.warn("headers already sent so no response sent"); 261 | } 262 | }); 263 | const httpServer = app.listen(process.env.PORT ?? 5050, () => { 264 | logger.info(`Server is running on port ${process.env.PORT ?? 5050}`); 265 | }); 266 | 267 | //httpServer.setTimeout(1_000 * 60 * 60 * 6); // 6 hours 268 | -------------------------------------------------------------------------------- /src/app.stateless.ts: -------------------------------------------------------------------------------- 1 | import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"; 2 | import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js"; 3 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 4 | import { config } from "dotenv"; 5 | import express, { 6 | type NextFunction, 7 | type Request, 8 | type Response, 9 | } from "express"; 10 | import createLogger from "logging"; 11 | import { InvalidAccessTokenError } from "./lib/errors"; 12 | import { ExtendedProxyOAuthServerProvider } from "./lib/extended-oauth-proxy-provider"; 13 | import InMemoryStorage from "./lib/storage/in-memory"; 14 | import { RedisStorage } from "./lib/storage/redis"; 15 | import type { OAuthProxyStorageManager } from "./lib/types"; 16 | import { server } from "./mcp-server"; 17 | config(); 18 | 19 | const logger = createLogger(__filename.split("/").pop() ?? "", { 20 | debugFunction: (...args) => { 21 | console.log(...args); 22 | }, 23 | }); 24 | const { 25 | OAUTH_ISSUER_URL, 26 | OAUTH_AUTHORIZATION_URL, 27 | OAUTH_TOKEN_URL, 28 | OAUTH_REVOCATION_URL, 29 | OAUTH_REGISTRATION_URL, 30 | THIS_HOSTNAME, 31 | } = process.env; 32 | 33 | if ( 34 | !OAUTH_ISSUER_URL || 35 | !OAUTH_AUTHORIZATION_URL || 36 | !OAUTH_TOKEN_URL || 37 | !OAUTH_REGISTRATION_URL || 38 | !THIS_HOSTNAME 39 | ) { 40 | throw new Error("Missing environment variables"); 41 | } 42 | 43 | // NOTE ideally we don't do this in memory since it's not horizontally scalable easily 44 | // but these are stateful objects with connections from the client so they can't just 45 | // be written to a database. 46 | const transports: { 47 | streamable: { [sessionId: string]: StreamableHTTPServerTransport }; 48 | } = { 49 | streamable: {}, 50 | }; 51 | 52 | let storageStrategy: OAuthProxyStorageManager; 53 | if (process.env.TOKEN_STORAGE_STRATEGY === "redis") { 54 | logger.info("Using redis storage strategy!"); 55 | storageStrategy = RedisStorage; 56 | } else { 57 | logger.warn( 58 | "Using in-memory storage strategy. DO NOT USE THIS IN PRODUCTION!", 59 | ); 60 | storageStrategy = InMemoryStorage; 61 | } 62 | 63 | process.env.TOKEN_STORAGE_STRATEGY === "memory" 64 | ? InMemoryStorage 65 | : RedisStorage; 66 | 67 | const app = express(); 68 | app.use(express.json()); 69 | app.use(express.urlencoded({ extended: false })); 70 | 71 | // Set up the OAuth Proxy provider; configured in .env to use Naptha's Auth0 tenant 72 | const proxyProvider = new ExtendedProxyOAuthServerProvider({ 73 | endpoints: { 74 | authorizationUrl: `${OAUTH_AUTHORIZATION_URL}`, 75 | tokenUrl: `${OAUTH_TOKEN_URL}`, 76 | revocationUrl: OAUTH_REVOCATION_URL, 77 | registrationUrl: `${OAUTH_REGISTRATION_URL}`, 78 | }, 79 | 80 | storageManager: storageStrategy, // configure with process.env.TOKEN_STORAGE_STRATEGY 81 | }); 82 | 83 | // Set up the middleware that verifies the issued bearer tokens. Note that these are NOT 84 | // the auth tokens from the upstream IDP. 85 | const bearerAuthMiddleware = requireBearerAuth({ 86 | provider: proxyProvider, 87 | requiredScopes: [], 88 | }); 89 | 90 | // Mount the router that handles the OAuth Proxy's endoints, discovery etc. 91 | app.use( 92 | mcpAuthRouter({ 93 | provider: proxyProvider, 94 | issuerUrl: new URL(`${OAUTH_ISSUER_URL}`), // address of issuer, auth0 95 | baseUrl: new URL(`${THIS_HOSTNAME}`), // address of local server 96 | }), 97 | ); 98 | 99 | /** 100 | * Set up the streamable HTTP MCP router 101 | */ 102 | app.use("/", async (req, res, next) => { 103 | logger.debug(req.method, req.url, req.headers, req.body); 104 | await next(); 105 | logger.debug(res.headersSent, res.statusCode); 106 | }); 107 | app.post("/mcp", async (req: Request, res: Response, next: NextFunction) => { 108 | logger.debug("POST /mcp"); 109 | 110 | const transport: StreamableHTTPServerTransport = 111 | new StreamableHTTPServerTransport({ 112 | sessionIdGenerator: undefined, // explicitly disable session ID generation since stateless 113 | }); 114 | await server.connect(transport); 115 | await transport.handleRequest(req, res, req.body); 116 | 117 | res.on("close", () => { 118 | console.log("Closing connection"); 119 | transport.close(); 120 | server.close(); 121 | }); 122 | }); 123 | 124 | app.use("/mcp", async (req: Request, res: Response, next: NextFunction) => { 125 | if (req.method === "GET" || req.method === "DELETE") { 126 | console.log(`Unsupported ${req.method} ${req.url} to stateless server`); 127 | res.writeHead(405).json({ 128 | jsonrpc: "2.0", 129 | error: { 130 | code: -32000, 131 | message: "Method not allowed.", 132 | }, 133 | id: null, 134 | }); 135 | } 136 | return next(); 137 | }); 138 | 139 | app.use((error: Error, req: Request, res: Response, next: NextFunction) => { 140 | logger.info("Error", error); 141 | if (!res.headersSent) { 142 | if (error instanceof InvalidAccessTokenError) { 143 | res.status(401).json({ 144 | jsonrpc: "2.0", 145 | error: { 146 | code: -32_000, 147 | message: "Invalid access token", 148 | }, 149 | id: null, 150 | }); 151 | } else { 152 | res.status(500).json({ 153 | jsonrpc: "2.0", 154 | error: { 155 | code: -32_000, 156 | message: "Internal server error", 157 | }, 158 | id: null, 159 | }); 160 | } 161 | } else { 162 | logger.warn("headers already sent so no response sent"); 163 | } 164 | }); 165 | const httpServer = app.listen(process.env.PORT ?? 5050, () => { 166 | logger.info(`Server is running on port ${process.env.PORT ?? 5050}`); 167 | }); 168 | 169 | //httpServer.setTimeout(1_000 * 60 * 60 * 6); // 6 hours 170 | -------------------------------------------------------------------------------- /src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | export class InvalidAccessTokenError extends Error {} 2 | -------------------------------------------------------------------------------- /src/lib/extended-oauth-proxy-provider.ts: -------------------------------------------------------------------------------- 1 | import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js"; 2 | import { 3 | InvalidTokenError, 4 | ServerError, 5 | } from "@modelcontextprotocol/sdk/server/auth/errors.js"; 6 | 7 | import type { AuthorizationParams } from "@modelcontextprotocol/sdk/server/auth/provider.js"; 8 | import { 9 | ProxyOAuthServerProvider, 10 | type ProxyOptions, 11 | } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js"; 12 | import { 13 | type OAuthClientInformationFull, 14 | OAuthClientInformationFullSchema, 15 | type OAuthTokens, 16 | OAuthTokensSchema, 17 | } from "@modelcontextprotocol/sdk/shared/auth.js"; 18 | import type { Response } from "express"; 19 | import createLogger from "logging"; 20 | import type { OAuthProxyStorageManager } from "./types"; 21 | 22 | const logger = createLogger(__filename.split("/").pop() ?? "", { 23 | debugFunction: (...args) => { 24 | console.log(...args); 25 | }, 26 | }); 27 | 28 | export type ExtendedOAuthTokens = OAuthTokens & { 29 | id_token?: string; 30 | }; 31 | 32 | /** 33 | * This type extends the ProxyOptions to add a saveClient method. 34 | * This can be provided by the server implementation for storing client information. 35 | */ 36 | export type ExtendedProxyOptions = Omit< 37 | ProxyOptions, 38 | "getClient" | "verifyAccessToken" 39 | > & { 40 | storageManager: OAuthProxyStorageManager; 41 | }; 42 | 43 | /** 44 | * This class extends the ProxyOAuthServerProvider to add a saveClient method. 45 | * That can be provided by the server implementation for storing client information. 46 | * 47 | * This way we don't have to hard-code return values like in the example 48 | */ 49 | export class ExtendedProxyOAuthServerProvider extends ProxyOAuthServerProvider { 50 | public readonly storageManager: OAuthProxyStorageManager; 51 | 52 | constructor(options: ExtendedProxyOptions) { 53 | // call the super constructor, but instead of having the user specify a custom getClient function like in the middleware, 54 | // we'll use the storageManager.getClient function 55 | super({ 56 | ...options, 57 | getClient: options.storageManager.getClient, 58 | verifyAccessToken: async (locallyIssuedAccessToken: string) => { 59 | const data = await this.storageManager.getAccessToken( 60 | locallyIssuedAccessToken, 61 | ); 62 | if (!data) { 63 | // This will return a 401 to the client, resulting in auth 64 | throw new InvalidTokenError("Invalid access token"); 65 | } 66 | return { 67 | token: locallyIssuedAccessToken, // NOT the upstream IDP token. 68 | scopes: data.scopes, 69 | clientId: data.clientId, 70 | expiresInSeconds: data.expiresInSeconds, 71 | }; 72 | }, 73 | }); 74 | this.storageManager = options.storageManager; 75 | } 76 | 77 | public override get clientsStore(): OAuthRegisteredClientsStore { 78 | const registrationUrl = this._endpoints.registrationUrl; 79 | return { 80 | getClient: this.storageManager.getClient, 81 | ...(registrationUrl && { 82 | registerClient: async (client: OAuthClientInformationFull) => { 83 | const response = await fetch(registrationUrl, { 84 | method: "POST", 85 | headers: { 86 | "Content-Type": "application/json", 87 | }, 88 | body: JSON.stringify(client), 89 | }); 90 | 91 | if (!response.ok) { 92 | throw new ServerError( 93 | `Client registration failed: ${response.status}`, 94 | ); 95 | } 96 | 97 | const data = await response.json(); 98 | const parsedClient = OAuthClientInformationFullSchema.parse(data); 99 | 100 | /** 101 | * NOTE this is the only change to this function from the original implementation 102 | * There's nowehere else that this information can be accessed. 103 | * 104 | * See @file{src/server/auth/handlers/register.ts} 105 | */ 106 | await this.storageManager.saveClient( 107 | parsedClient.client_id, 108 | parsedClient, 109 | ); 110 | 111 | return parsedClient; 112 | }, 113 | }), 114 | }; 115 | } 116 | 117 | /** 118 | * Using this overridden method so we can do some logging and stuff 119 | */ 120 | public override async exchangeAuthorizationCode( 121 | client: OAuthClientInformationFull, 122 | authorizationCode: string, 123 | codeVerifier?: string, 124 | ): Promise { 125 | const redirectUri = client.redirect_uris[0]; 126 | if (redirectUri) { 127 | logger.debug( 128 | "Exchanging authorization code with client redirect URI: ", 129 | redirectUri, 130 | authorizationCode, 131 | codeVerifier, 132 | ); 133 | } else { 134 | logger.error( 135 | "No redirect URI found for client", 136 | client.client_id, 137 | client, 138 | ); 139 | throw new ServerError("No redirect URI found for client"); 140 | } 141 | const params = new URLSearchParams({ 142 | grant_type: "authorization_code", 143 | client_id: client.client_id, 144 | redirect_uri: redirectUri, 145 | code: authorizationCode, 146 | }); 147 | 148 | if (client.client_secret) { 149 | params.append("client_secret", client.client_secret); 150 | } 151 | 152 | if (codeVerifier) { 153 | params.append("code_verifier", codeVerifier); 154 | } 155 | 156 | const response = await fetch(this._endpoints.tokenUrl, { 157 | method: "POST", 158 | headers: { 159 | "Content-Type": "application/x-www-form-urlencoded", 160 | }, 161 | body: params.toString(), 162 | }); 163 | 164 | if (!response.ok) { 165 | logger.error( 166 | "Token exchange failed", 167 | response.status, 168 | response.statusText, 169 | ); 170 | logger.error("JSON:", await response.json()); 171 | throw new ServerError(`Token exchange failed: ${response.status}`); 172 | } 173 | 174 | const data = (await response.json()) as ExtendedOAuthTokens; 175 | logger.debug("Saving access token", data.access_token); 176 | const locallyIssuedAccessToken = await this.storageManager.saveAccessToken( 177 | { 178 | accessToken: data.access_token, 179 | idToken: data.id_token, 180 | refreshToken: data.refresh_token, 181 | clientId: client.client_id, 182 | scope: data.scope ?? "", 183 | }, 184 | data.expires_in ?? 86400, // default to 1 day 185 | ); 186 | 187 | return OAuthTokensSchema.parse({ 188 | ...data, 189 | access_token: locallyIssuedAccessToken, 190 | }); 191 | } 192 | 193 | public override async authorize( 194 | client: OAuthClientInformationFull, 195 | params: AuthorizationParams, 196 | res: Response, 197 | ): Promise { 198 | // Start with required OAuth parameters 199 | const targetUrl = new URL(this._endpoints.authorizationUrl); 200 | const searchParams = new URLSearchParams({ 201 | client_id: client.client_id, 202 | response_type: "code", 203 | redirect_uri: params.redirectUri, 204 | code_challenge: params.codeChallenge, 205 | code_challenge_method: "S256", 206 | }); 207 | 208 | logger.debug("authorize", { 209 | client, 210 | params, 211 | targetUrl, 212 | searchParams, 213 | }); 214 | 215 | // Add optional standard OAuth parameters 216 | if (params.state) searchParams.set("state", params.state); 217 | 218 | searchParams.set( 219 | "scope", 220 | params.scopes?.length 221 | ? params.scopes.join(" ") 222 | : ["email", "profile", "openid"].join(" "), 223 | ); 224 | 225 | targetUrl.search = searchParams.toString(); 226 | res.redirect(targetUrl.toString()); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/lib/storage/in-memory.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "node:crypto"; 2 | /** 3 | * This file presents a simple in-memory storage implementation for the OAuth proxy. useful if you are locally debugging 4 | * or don't want to set up a database or something. don't use this in production 5 | */ 6 | import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; 7 | import type { 8 | GetAccessTokenFunction, 9 | OAuthProxyStorageManager, 10 | } from "../types"; 11 | 12 | // Local storage for the OAuth Proxy 13 | const clients: Record = {}; 14 | const accessTokens: Record< 15 | string, 16 | Awaited> 17 | > = {}; 18 | 19 | export const InMemoryStorage: OAuthProxyStorageManager = { 20 | saveClient: async (clientId: string, data: OAuthClientInformationFull) => { 21 | clients[clientId] = data; 22 | }, 23 | getClient: async (clientId: string) => { 24 | return clients[clientId]; 25 | }, 26 | saveAccessToken: async ( 27 | { accessToken, idToken, clientId, scope }, 28 | expiresInSeconds: number, 29 | ) => { 30 | const locallyIssuedAccessToken = randomBytes(64).toString("hex"); 31 | 32 | // save the read access token and other information under the "proxied" access token 33 | accessTokens[locallyIssuedAccessToken] = { 34 | scopes: scope?.split(" ") ?? [], 35 | clientId, 36 | idToken: idToken ?? "", 37 | accessToken: accessToken, 38 | expiresInSeconds, 39 | }; 40 | setTimeout(() => { 41 | delete accessTokens[locallyIssuedAccessToken]; 42 | }, expiresInSeconds * 1000); 43 | return locallyIssuedAccessToken; 44 | }, 45 | getAccessToken: async (locallyIssuedAccessToken: string) => { 46 | return accessTokens[locallyIssuedAccessToken]; 47 | }, 48 | }; 49 | 50 | export default InMemoryStorage; 51 | -------------------------------------------------------------------------------- /src/lib/storage/redis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file defines a Redis-based storage implementation for the OAuth proxy. 3 | * This is useful for production use, as it allows the OAuth proxy to be horizontally scalable (if you can solve the transport in-memory issue...) 4 | * 5 | */ 6 | 7 | import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; 8 | import Redis from "ioredis"; 9 | import createLogger from "logging"; 10 | import { randomBytes } from "node:crypto"; 11 | import type { OAuthProxyStorageManager } from "../types"; 12 | 13 | const logger = createLogger("RedisStorage", { 14 | debugFunction: (...args) => console.log(...args), 15 | }); 16 | 17 | const redis = new Redis(process.env.REDIS_DSN ?? "redis://localhost:6379"); 18 | 19 | redis.on("connecting", () => logger.debug("Redis connecting...")); 20 | redis.on("connect", () => logger.info("Redis connected!")); 21 | redis.on("error", (err) => logger.error("Redis error", err)); 22 | redis.on("close", () => logger.info("Redis closed!")); 23 | 24 | export const RedisStorage: OAuthProxyStorageManager = { 25 | saveClient: async (clientId: string, data: OAuthClientInformationFull) => { 26 | await redis.set(clientId, JSON.stringify(data)); 27 | }, 28 | getClient: async (clientId: string) => { 29 | const data = await redis.get(clientId); 30 | return data ? JSON.parse(data) : undefined; 31 | }, 32 | saveAccessToken: async ( 33 | { accessToken, idToken, refreshToken, clientId, scope }, 34 | expiresInSeconds: number, 35 | ) => { 36 | const locallyIssuedAccessToken = randomBytes(64).toString("hex"); 37 | await redis.setex( 38 | locallyIssuedAccessToken, 39 | expiresInSeconds, 40 | JSON.stringify({ 41 | idToken, 42 | refreshToken, 43 | clientId, 44 | scopes: scope.split(" "), 45 | accessToken, 46 | }), 47 | ); 48 | return locallyIssuedAccessToken; 49 | }, 50 | getAccessToken: async (locallyIssuedAccessToken: string) => { 51 | const data = await redis.get(locallyIssuedAccessToken); 52 | return data ? JSON.parse(data) : undefined; 53 | }, 54 | }; 55 | 56 | export default RedisStorage; 57 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; 2 | 3 | // Define JSON types 4 | export type JsonValue = 5 | | string 6 | | number 7 | | boolean 8 | | null 9 | | JsonValue[] 10 | | { [key: string]: JsonValue }; 11 | 12 | export type JsonObject = { [key: string]: JsonValue }; 13 | 14 | // Define the type for the save function 15 | export type SaveClientInfoFunction = ( 16 | clientId: string, 17 | data: OAuthClientInformationFull, 18 | ) => Promise; 19 | export type GetClientInfoFunction = ( 20 | clientId: string, 21 | ) => Promise; 22 | 23 | export type SaveAccessTokenFunction = ( 24 | { 25 | accessToken, 26 | idToken, 27 | refreshToken, 28 | clientId, 29 | scope, 30 | }: { 31 | accessToken: string; 32 | idToken?: string; 33 | refreshToken?: string; 34 | clientId: string; 35 | scope: string; 36 | }, 37 | expiresInSeconds: number, 38 | ) => Promise; 39 | export type GetAccessTokenFunction = (accessToken: string) => Promise< 40 | | { 41 | scopes: Array; 42 | clientId: string; 43 | accessToken: string; 44 | idToken?: string; 45 | refreshToken?: string; 46 | expiresInSeconds: number; 47 | } 48 | | undefined 49 | >; 50 | 51 | export type OAuthProxyStorageManager = { 52 | saveClient: SaveClientInfoFunction; 53 | getClient: GetClientInfoFunction; 54 | saveAccessToken: SaveAccessTokenFunction; 55 | getAccessToken: GetAccessTokenFunction; 56 | }; 57 | -------------------------------------------------------------------------------- /src/mcp-server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | 5 | import { z } from "zod"; 6 | 7 | export const server = new McpServer({ 8 | name: "Math-MCP-Server", 9 | version: "1.0.0", 10 | }); 11 | 12 | server.tool( 13 | "add", 14 | "Add two numbers", 15 | { l: z.number(), r: z.number() }, 16 | async ({ l, r }) => ({ 17 | content: [ 18 | { 19 | type: "text", 20 | text: String(l + r), 21 | }, 22 | ], 23 | }), 24 | ); 25 | 26 | server.tool( 27 | "divide", 28 | "Divide two numbers", 29 | { l: z.number(), r: z.number() }, 30 | async ({ l, r }) => ({ 31 | content: [ 32 | { 33 | type: "text", 34 | text: String(l / r), 35 | }, 36 | ], 37 | }), 38 | ); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "target": "ESNext", 8 | "module": "ESNext", 9 | "moduleDetection": "force", 10 | "jsx": "react-jsx", 11 | "allowJs": true, 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": false, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": false, 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } --------------------------------------------------------------------------------