├── .cursor
└── rules
│ └── mcp-framework.mdc
├── .editorconfig
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierrc
├── .release-please-config.json
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.js
├── package-lock.json
├── package.json
├── src
├── auth
│ ├── index.ts
│ ├── providers
│ │ ├── apikey.ts
│ │ └── jwt.ts
│ └── types.ts
├── cli
│ ├── framework
│ │ ├── build-cli.ts
│ │ └── build.ts
│ ├── index.ts
│ ├── project
│ │ ├── add-prompt.ts
│ │ ├── add-resource.ts
│ │ ├── add-tool.ts
│ │ └── create.ts
│ ├── templates
│ │ └── readme.ts
│ └── utils
│ │ ├── string-utils.ts
│ │ └── validate-project.ts
├── core
│ ├── Logger.ts
│ └── MCPServer.ts
├── index.ts
├── loaders
│ ├── promptLoader.ts
│ ├── resourceLoader.ts
│ └── toolLoader.ts
├── prompts
│ └── BasePrompt.ts
├── resources
│ └── BaseResource.ts
├── tools
│ └── BaseTool.ts
├── transports
│ ├── base.ts
│ ├── http
│ │ ├── server.ts
│ │ └── types.ts
│ ├── sse
│ │ ├── server.ts
│ │ └── types.ts
│ ├── stdio
│ │ └── server.ts
│ └── utils
│ │ ├── image-handler.ts
│ │ └── ping-message.ts
└── utils
│ └── headers.ts
└── tsconfig.json
/.cursor/rules/mcp-framework.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: false
5 | ---
6 | We are in the mcp-framework , a typescript framework that allows construction of model context protocol (mcp) servers.
7 | We are implementing the new specification:
8 | Overview
9 | MCP provides a standardized way for applications to:
10 |
11 | Share contextual information with language models
12 | Expose tools and capabilities to AI systems
13 | Build composable integrations and workflows
14 | The protocol uses JSON-RPC 2.0 messages to establish communication between:
15 |
16 | Hosts: LLM applications that initiate connections
17 | Clients: Connectors within the host application
18 | Servers: Services that provide context and capabilities
19 | MCP takes some inspiration from the Language Server Protocol, which standardizes how to add support for programming languages across a whole ecosystem of development tools. In a similar way, MCP standardizes how to integrate additional context and tools into the ecosystem of AI applications.
20 |
21 | Key Details
22 | Base Protocol
23 | JSON-RPC message format
24 | Stateful connections
25 | Server and client capability negotiation
26 | Features
27 | Servers offer any of the following features to clients:
28 |
29 | Resources: Context and data, for the user or the AI model to use
30 | Prompts: Templated messages and workflows for users
31 | Tools: Functions for the AI model to execute
32 |
33 | Here is an example of how a user creats an mcp server using mcp-framework as the library: ## src/tools/ExampleTool.ts
34 |
35 | ```ts
36 | import { MCPTool } from "mcp-framework";
37 | import { z } from "zod";
38 |
39 | interface ExampleInput {
40 | message: string;
41 | }
42 |
43 | class ExampleTool extends MCPTool {
44 | name = "example_tool";
45 | description = "An example tool that processes messages";
46 |
47 | schema = {
48 | message: {
49 | type: z.string(),
50 | description: "Message to process",
51 | },
52 | };
53 |
54 | async execute(input: ExampleInput) {
55 | return `Processed: ${input.message}`;
56 | }
57 | }
58 |
59 | export default ExampleTool;
60 | ```
61 |
62 | ## src/index.ts
63 |
64 | ```ts
65 | import { MCPServer } from "mcp-framework";
66 |
67 | const server = new MCPServer({transport:{
68 | type:"http-stream",
69 | options:{
70 | port:1337,
71 | cors: {
72 | allowOrigin:"*"
73 | }
74 | }
75 | }});
76 |
77 | server.start();
78 |
79 | ```
80 |
81 | ## package.json
82 |
83 | ```json
84 | {
85 | "name": "http2-hi",
86 | "version": "0.0.1",
87 | "description": "http2-hi MCP server",
88 | "type": "module",
89 | "bin": {
90 | "http2-hi": "./dist/index.js"
91 | },
92 | "files": [
93 | "dist"
94 | ],
95 | "scripts": {
96 | "build": "tsc && mcp-build",
97 | "watch": "tsc --watch",
98 | "start": "node dist/index.js"
99 | },
100 | "dependencies": {
101 | "mcp-framework": "^0.2.12-beta.4",
102 | "zod": "^3.24.4"
103 | },
104 | "devDependencies": {
105 | "@types/node": "^20.11.24",
106 | "typescript": "^5.3.3"
107 | },
108 | "engines": {
109 | "node": ">=18.19.0"
110 | }
111 | }
112 |
113 | ```
114 |
115 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: write
11 | pull-requests: write
12 |
13 | jobs:
14 | release-please:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: googleapis/release-please-action@v4
18 | with:
19 | config-file: '.release-please-config.json'
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | README
2 | .env
3 | .vscode
4 | .idea
5 | .DS_Store
6 | dist
7 | node_modules
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "es5",
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2,
7 | "endOfLine": "auto"
8 | }
9 |
--------------------------------------------------------------------------------
/.release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | ".": {
4 | "release-type": "node",
5 | "bump-minor-pre-major": true,
6 | "bump-patch-for-minor-pre-major": true,
7 | "changelog-path": "CHANGELOG.md",
8 | "pull-request-title-pattern": "chore: release ${version}"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "0.2.13"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.2.13](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.12...mcp-framework-v0.2.13) (2025-05-23)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * properly support required/optional tool input schema ([1583603](https://github.com/QuantGeekDev/mcp-framework/commit/1583603b2613b8c7c57ebbd52fb5c8159d0b8d13))
14 |
15 | ## [0.2.12](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.11...mcp-framework-v0.2.12) (2025-05-23)
16 |
17 |
18 | ### Features
19 |
20 | * bump mcp ts sdk version ([d9cc845](https://github.com/QuantGeekDev/mcp-framework/commit/d9cc8450d39e89f36e39e78bb4d1946cf5f858d3))
21 | * enhanced cursor rule with example ([d3b54d4](https://github.com/QuantGeekDev/mcp-framework/commit/d3b54d4619e669ad322484b074a82aae27d61ae9))
22 | * replace custom implementation with sdk delegation ([1b5b8e7](https://github.com/QuantGeekDev/mcp-framework/commit/1b5b8e7cbe354056856a565b21b8908eb93ac2ba))
23 |
24 | ## [0.2.11](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.10...mcp-framework-v0.2.11) (2025-03-30)
25 |
26 |
27 | ### Features
28 |
29 | * implement resources/templates/list handler ([ff73ff0](https://github.com/QuantGeekDev/mcp-framework/commit/ff73ff084860c12a0aae15757c39ab6eeef5a543))
30 | * implement resources/templates/list handler ([0dabfc0](https://github.com/QuantGeekDev/mcp-framework/commit/0dabfc04370535ecbe9d31f4d2b54ac876032b93))
31 |
32 | ## [0.2.10](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.9...mcp-framework-v0.2.10) (2025-03-30)
33 |
34 |
35 | ### Features
36 |
37 | * add optional skip install param ([d77e6e9](https://github.com/QuantGeekDev/mcp-framework/commit/d77e6e9df5a6d989dc7fbfa25b2cbe3b56e50260))
38 | * add optional skip install param ([318dbc7](https://github.com/QuantGeekDev/mcp-framework/commit/318dbc798e673678c38a468e6a898e9834cdfa7d))
39 | * add skip example option to cli ([df733f9](https://github.com/QuantGeekDev/mcp-framework/commit/df733f999e9837012220a7696d993ef734eee393))
40 | * add skip example option to cli ([c02809f](https://github.com/QuantGeekDev/mcp-framework/commit/c02809f185270bdc462f367b3a0e52a6e4d4300d))
41 |
42 |
43 | ### Bug Fixes
44 |
45 | * Fixes ESLint 'no-case-declarations' error in HTTP transport by adding block scope to the default switch case. ([25ed8e6](https://github.com/QuantGeekDev/mcp-framework/commit/25ed8e6de845f72ea2967ff64b981e445ae48249))
46 | * make ping conform with the spec ([aa46eb8](https://github.com/QuantGeekDev/mcp-framework/commit/aa46eb8199c95e4b1024e84d5a616e0cc420cd64))
47 | * **transports:** Conform SSE/HTTP streams to MCP spec and improve logging ([9d9ef2a](https://github.com/QuantGeekDev/mcp-framework/commit/9d9ef2aa2ddea52c37133d4842d95d168ea5e190))
48 | * **transports:** follow spec guideline ([208599d](https://github.com/QuantGeekDev/mcp-framework/commit/208599ddaafbf58eddaf4d5d6492a26e1effbbc6))
49 |
50 | ## [0.2.9](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.8...mcp-framework-v0.2.9) (2025-03-29)
51 |
52 |
53 | ### Features
54 |
55 | * add linting ([e71ebb5](https://github.com/QuantGeekDev/mcp-framework/commit/e71ebb5d538cb03510633bac0cf41bd318a0eab9))
56 | * add linting ([181e634](https://github.com/QuantGeekDev/mcp-framework/commit/181e634da0cf97f2f40ae8b7e4fd4e74935a1c3c))
57 | * add sse resumability ([4b47edb](https://github.com/QuantGeekDev/mcp-framework/commit/4b47edb243286a9f32bb81655bd0b51c2c4695e2))
58 | * add sse resumability ([e20b7cc](https://github.com/QuantGeekDev/mcp-framework/commit/e20b7cc887dd4b080d38e01d21ac2ef3e63843d1))
59 | * improve error handling for sse ([a5644af](https://github.com/QuantGeekDev/mcp-framework/commit/a5644af425563aca22fdf46ec24b1f547a5d9143))
60 | * improve error handling for sse ([ba1646b](https://github.com/QuantGeekDev/mcp-framework/commit/ba1646be8da98c4d86b55313c515903c692d8f9f))
61 |
62 |
63 | ### Bug Fixes
64 |
65 | * close sse stream after post ([eef96b4](https://github.com/QuantGeekDev/mcp-framework/commit/eef96b4c429af9c2f7083352c4bd45d645927352))
66 | * close sse stream after post ([d6ea60d](https://github.com/QuantGeekDev/mcp-framework/commit/d6ea60deb5283551ce9730e2d20e38ffe8f6c711))
67 | * detect tools capability using toolLoader ([c5d34a5](https://github.com/QuantGeekDev/mcp-framework/commit/c5d34a5be034ee0fb22e888d7696d64ac703e727))
68 | * detect tools capability using toolLoader ([1e4c71f](https://github.com/QuantGeekDev/mcp-framework/commit/1e4c71f71634a72b7c146d84a21582e6e9d5fd3b))
69 | * enforce that initialize request cannot be part of JSON-RPC batch ([452740c](https://github.com/QuantGeekDev/mcp-framework/commit/452740c9bdba8df014a4cd3d5149e78190c05058))
70 | * enforce that initialize request cannot be part of JSON-RPC batch ([6cccf54](https://github.com/QuantGeekDev/mcp-framework/commit/6cccf54c18a6554c243d7dfdc286dcc3e92cb75f))
71 | * import path utilities to resolve build errors ([5a7672c](https://github.com/QuantGeekDev/mcp-framework/commit/5a7672cca08dc21d527dc1d4b6a9cbdf809938bc))
72 | * import path utilities to resolve build errors ([534d0de](https://github.com/QuantGeekDev/mcp-framework/commit/534d0de047e3d29f214b088c8fdad2d25444b344))
73 | * project validation not working on windows ([fc506d3](https://github.com/QuantGeekDev/mcp-framework/commit/fc506d3b13a7c8c25647f6d2d8b278fa25e22ea5))
74 |
75 | ## [0.2.8](https://github.com/QuantGeekDev/mcp-framework/compare/mcp-framework-v0.2.7...mcp-framework-v0.2.8) (2025-03-28)
76 |
77 |
78 | ### Features
79 |
80 | * add auth ([6338d14](https://github.com/QuantGeekDev/mcp-framework/commit/6338d14b6b8ad020ab41b2c2d3cf843d660db2fc))
81 | * add Base Tool ([6b51594](https://github.com/QuantGeekDev/mcp-framework/commit/6b51594aaaedf0c13c418055d2b2039e919cc2d3))
82 | * add basic sse support ([e8b808f](https://github.com/QuantGeekDev/mcp-framework/commit/e8b808f1967a9255a94cb9922169724e0a0bb145))
83 | * add build before start. this one is for you, vibe coders ;) ([fa43fa8](https://github.com/QuantGeekDev/mcp-framework/commit/fa43fa81195981688adf30689d1cbd65fdb29892))
84 | * add cli tool ([91101ee](https://github.com/QuantGeekDev/mcp-framework/commit/91101ee9671eb8489d7cb69d2b596d201a204cb1))
85 | * add cors configuration ([5d3f27f](https://github.com/QuantGeekDev/mcp-framework/commit/5d3f27f0f2ce61551b1c66dbf9d3aa7f640606ff))
86 | * add execa ([4d44864](https://github.com/QuantGeekDev/mcp-framework/commit/4d44864db3abbd41cbf02da380c597e92d2de39d))
87 | * add execa ([d68cfb6](https://github.com/QuantGeekDev/mcp-framework/commit/d68cfb6b43034893a6de3018793164d5d334f5fe))
88 | * add find-up ([09c767f](https://github.com/QuantGeekDev/mcp-framework/commit/09c767fe82dbf53403543905f8e056a9e2c9a3df))
89 | * add find-up ([9f90df9](https://github.com/QuantGeekDev/mcp-framework/commit/9f90df97a5cf3777a5910c0a806a6fbc70dee61e))
90 | * Add HTTP Stream transport with improved session handling and CORS support ([04ff8a5](https://github.com/QuantGeekDev/mcp-framework/commit/04ff8a5453b56e912d157356bb05a8cb6c987c41))
91 | * add image support ([ab023ba](https://github.com/QuantGeekDev/mcp-framework/commit/ab023ba082af9d7e560a498e1e77b93e69331d74))
92 | * add image support ([9c8ca20](https://github.com/QuantGeekDev/mcp-framework/commit/9c8ca201f792a2386b112e67d1e664ca78d2e2d1))
93 | * add index ([62873b9](https://github.com/QuantGeekDev/mcp-framework/commit/62873b9fb67d41cff0fdca647b4d1a0ff4d35e6b))
94 | * add keywords to package ([95ea9a7](https://github.com/QuantGeekDev/mcp-framework/commit/95ea9a7adb25fb5dd844a944ef1a5cc7ccfaeb25))
95 | * Add LICENSE ([2d3612a](https://github.com/QuantGeekDev/mcp-framework/commit/2d3612a89dcd27b1567480378b318eeb8e838863))
96 | * add logging ([d637a65](https://github.com/QuantGeekDev/mcp-framework/commit/d637a6560b17b09c5383b6b1ca7838f08c5fc780))
97 | * add mcp-build cli ([27cb517](https://github.com/QuantGeekDev/mcp-framework/commit/27cb517306deaf2b34646d9b2ac617dc70c476f5))
98 | * add MCPServer ([807f04d](https://github.com/QuantGeekDev/mcp-framework/commit/807f04ddbfe28598bb26c0dce640dcc0909d739a))
99 | * add MCPTool abstraction ([4d5fb39](https://github.com/QuantGeekDev/mcp-framework/commit/4d5fb398efd4a253f1ea23e8307690ecb84f8009))
100 | * add MCPTool abstraction ([3458e78](https://github.com/QuantGeekDev/mcp-framework/commit/3458e78233437c9afd71b9195424659e62aa08f3))
101 | * add prompt capabilities ([9ca6a0f](https://github.com/QuantGeekDev/mcp-framework/commit/9ca6a0fc19638c2428c060bdb3996d49c5c31e55))
102 | * add prompt capabilities ([019f409](https://github.com/QuantGeekDev/mcp-framework/commit/019f40949ea66fc4d1ca1969aaa57c8943e81036))
103 | * add readme ([752024e](https://github.com/QuantGeekDev/mcp-framework/commit/752024ed92213c3c27a2b9a5a702f823e835e167))
104 | * add README ([b0a371c](https://github.com/QuantGeekDev/mcp-framework/commit/b0a371c1b3a6275d249fdd89e67d7d791b573601))
105 | * add release-please workflow ([eb6c121](https://github.com/QuantGeekDev/mcp-framework/commit/eb6c121a1b22659747c7edaecf05d3cd39b2d92e))
106 | * add release-please workflow ([5a89670](https://github.com/QuantGeekDev/mcp-framework/commit/5a89670a1f797f8d91254d0b3f9f991fdc77148a))
107 | * add resources support ([e8b03d4](https://github.com/QuantGeekDev/mcp-framework/commit/e8b03d432fd09e7e25e3f272f735cebeb571a31b))
108 | * add sdk version logging ([8a05c48](https://github.com/QuantGeekDev/mcp-framework/commit/8a05c48431ff01b4fd68e178a055cfec41236b21))
109 | * add sdk version logging ([bb716db](https://github.com/QuantGeekDev/mcp-framework/commit/bb716dba19d75214f7f23973f526f4ab107fe187))
110 | * add toolLoader ([a44ffe7](https://github.com/QuantGeekDev/mcp-framework/commit/a44ffe7e7df099479ff516bc5fa0e9e2a5115bf0))
111 | * add typescript dependencies ([0ade3dc](https://github.com/QuantGeekDev/mcp-framework/commit/0ade3dc3627934884b8908202fe71b06a3a1c660))
112 | * bump up version ([85624c6](https://github.com/QuantGeekDev/mcp-framework/commit/85624c6bfe7dda5df0cfb6894b58bf8a6b39a99e))
113 | * bump version ([0d88a0f](https://github.com/QuantGeekDev/mcp-framework/commit/0d88a0fbd6b571ff578a96c85361930616371355))
114 | * bump version ([e227702](https://github.com/QuantGeekDev/mcp-framework/commit/e227702c87476bda82384c34efd871b13292b089))
115 | * bump version ([33b1776](https://github.com/QuantGeekDev/mcp-framework/commit/33b17764330126ee79bb4c9510d8f067b339fda3))
116 | * bump version ([a0e5a38](https://github.com/QuantGeekDev/mcp-framework/commit/a0e5a381ecc4b9fc6dee3a35609ead7297af905a))
117 | * bump version ([ea3085d](https://github.com/QuantGeekDev/mcp-framework/commit/ea3085d1deae39e8dc737be499564d6a9487654c))
118 | * bump version ([ddf74f6](https://github.com/QuantGeekDev/mcp-framework/commit/ddf74f6f142a69dcfe99af8def8b785b59c6d662))
119 | * bump version ([4c04edb](https://github.com/QuantGeekDev/mcp-framework/commit/4c04edbf0037f34041948bd9596a1d3cab4f4b34))
120 | * bump version ([0e6a21b](https://github.com/QuantGeekDev/mcp-framework/commit/0e6a21bbfc90ed9166692e4aea02d4e7d07a93d4))
121 | * bump version ([1d1cfef](https://github.com/QuantGeekDev/mcp-framework/commit/1d1cfef4542d95d66fb52b8bee61d0eb6766674a))
122 | * bump version number ([3a7f329](https://github.com/QuantGeekDev/mcp-framework/commit/3a7f329092c810d99d0cedd09ba11d902e941879))
123 | * enforce node version 20 ([8f1466a](https://github.com/QuantGeekDev/mcp-framework/commit/8f1466adfa7f6adc69070ed1d7feb7c8ffbca224))
124 | * enforce node version 20 ([bf4a4bb](https://github.com/QuantGeekDev/mcp-framework/commit/bf4a4bb427bedd948509eaf13b8a82222c98c006))
125 | * fix directory validation issue ([bf0e0d4](https://github.com/QuantGeekDev/mcp-framework/commit/bf0e0d4d331cf64a132b27c52b85d0115fe6f280))
126 | * fix directory validation issue ([bc6ab31](https://github.com/QuantGeekDev/mcp-framework/commit/bc6ab31396a05c898121ccc96a545e4a1ebc82ea))
127 | * HTTP stream transport implementation (v0.2.0-beta.16) ([d29fb5f](https://github.com/QuantGeekDev/mcp-framework/commit/d29fb5fc36ce67c4494a2a47e79645a2559c4f80))
128 | * lower node version to 18 ([77c8d1b](https://github.com/QuantGeekDev/mcp-framework/commit/77c8d1bcb4b26dbc0d8884269d9b5ddfe9b8864c))
129 | * make the mcp server config optional ([9538626](https://github.com/QuantGeekDev/mcp-framework/commit/9538626ff03a9534a72913436c011616b61163e2))
130 | * make toolLoader load tools automatically ([16f2793](https://github.com/QuantGeekDev/mcp-framework/commit/16f27930bf4d5251867f5a700fa1842d8f70ad07))
131 | * read default name and version from package json ([b5789af](https://github.com/QuantGeekDev/mcp-framework/commit/b5789af47c112eddece3764505e8e942dc6f3810))
132 | * remove vibe coder contingency ([36bbc88](https://github.com/QuantGeekDev/mcp-framework/commit/36bbc88d731f6aa2629ad85da3ab69e77508b74f))
133 | * update readme ([ac3725a](https://github.com/QuantGeekDev/mcp-framework/commit/ac3725a4fda0b0b14a9a5d3cea44674e191999dc))
134 | * update readme ([06af3b8](https://github.com/QuantGeekDev/mcp-framework/commit/06af3b8573e41360f528b21176fceeb4290f1197))
135 | * update readme ([e739ff6](https://github.com/QuantGeekDev/mcp-framework/commit/e739ff6d2570b134289c3190476af4fe7ca61d1c))
136 | * update README ([6c1efcd](https://github.com/QuantGeekDev/mcp-framework/commit/6c1efcde6bcef838aaacc52e6e41903025e5ecc5))
137 | * upgrade tool loader ([2b066e6](https://github.com/QuantGeekDev/mcp-framework/commit/2b066e6a0af46f9720ee266e019345fd6022eb3a))
138 |
139 |
140 | ### Bug Fixes
141 |
142 | * add release-please manifest file ([14bd878](https://github.com/QuantGeekDev/mcp-framework/commit/14bd878652ea3fdbf684749cb42752bccf675a6b))
143 | * build error ([adc7e48](https://github.com/QuantGeekDev/mcp-framework/commit/adc7e48d51b6075ab7624901033050e87ef1f632))
144 | * build errors with mcp-build ([27c255b](https://github.com/QuantGeekDev/mcp-framework/commit/27c255b119dbcc79575b4feae589c980972c8933))
145 | * build errors with mcp-build ([dc5b56e](https://github.com/QuantGeekDev/mcp-framework/commit/dc5b56e6b1bf7937b02eb29a7c33a6ca7b771eb6))
146 | * build issues ([2a3d12d](https://github.com/QuantGeekDev/mcp-framework/commit/2a3d12d1d5c0bd238cdd090e0e805cd4625ade32))
147 | * cli build issues ([f84266a](https://github.com/QuantGeekDev/mcp-framework/commit/f84266a29214e5de73bd52bc0135727882cadbef))
148 | * execa ([641614d](https://github.com/QuantGeekDev/mcp-framework/commit/641614d3b86450fbc850ee22898003438f5504cc))
149 | * execa ([e520f27](https://github.com/QuantGeekDev/mcp-framework/commit/e520f2718fb8c082bdd76431e1b63b9cd72a301e))
150 | * follow spec ([b9420c4](https://github.com/QuantGeekDev/mcp-framework/commit/b9420c45fbb44d65b4cf430dc69ee96294d7ad43))
151 | * follow spec ([938a13b](https://github.com/QuantGeekDev/mcp-framework/commit/938a13b4c7ae08f88a5ac4078dbac94b1f89f31d))
152 | * node spawning issue ([8b5d4fa](https://github.com/QuantGeekDev/mcp-framework/commit/8b5d4fafac022c15028abbf62a11fdf2a35207df))
153 | * Prevent duplicate builds by removing prepare script ([3584959](https://github.com/QuantGeekDev/mcp-framework/commit/3584959f6c3260c3299beeb6cc09c284f6206d84))
154 | * remove findup ([f281451](https://github.com/QuantGeekDev/mcp-framework/commit/f281451cb25264fcd80bf90ac7027fd1bbeaa1f4))
155 | * remove findup ([be6f5b0](https://github.com/QuantGeekDev/mcp-framework/commit/be6f5b076c0c764b4447a87eb0b75ee5cc607e42))
156 | * remove project name ([40789f3](https://github.com/QuantGeekDev/mcp-framework/commit/40789f3760f04a0dc117b6866b439e283ceec68a))
157 | * remove project name ([6e89e31](https://github.com/QuantGeekDev/mcp-framework/commit/6e89e3128cca93d2657fc2dc382fa0f0d658aaf6))
158 | * remove redundant prepare script from package.json and create.ts ([1ddff3f](https://github.com/QuantGeekDev/mcp-framework/commit/1ddff3f5ce4f8d7817a9c04db6432332957635a9))
159 | * scope ([84b72f6](https://github.com/QuantGeekDev/mcp-framework/commit/84b72f6f2988121277bffcb5eb0474a47c2940a1))
160 | * session id for initialization ([2743eaf](https://github.com/QuantGeekDev/mcp-framework/commit/2743eaf68994e890d319e9b2881ea47bb676d848))
161 | * session id for initialization ([061d152](https://github.com/QuantGeekDev/mcp-framework/commit/061d1522729392a49e8565569931d96ef7c57693))
162 | * sse reconnect issues ([fad62e3](https://github.com/QuantGeekDev/mcp-framework/commit/fad62e33b9fc3cc0df5bdfd96370dd6c2723dc57))
163 | * tool loader base dir ([636807c](https://github.com/QuantGeekDev/mcp-framework/commit/636807cea8eb93227a75725b7359828e24bc8d26))
164 | * tool loader base dir ([aa035a0](https://github.com/QuantGeekDev/mcp-framework/commit/aa035a0cd3da7d2187b2aae8d08da382b0a8d1f2))
165 | * tsc during project creation bug ([eb3a7bf](https://github.com/QuantGeekDev/mcp-framework/commit/eb3a7bf0f94ca9809cfede795425f30a31480664))
166 | * tsc during project creation bug ([2177d3e](https://github.com/QuantGeekDev/mcp-framework/commit/2177d3ebfee001242fb7fa6ac989cee9e3c05a1b))
167 | * tsconfig not found ([4d2062f](https://github.com/QuantGeekDev/mcp-framework/commit/4d2062fe5e8f1984b770cd5389d4c2c8f84c7cff))
168 | * tsconfig not found ([ec87841](https://github.com/QuantGeekDev/mcp-framework/commit/ec87841580cf9d4ea1ab9e5e3765b68f01a483be))
169 | * wrong index created ([b6f186c](https://github.com/QuantGeekDev/mcp-framework/commit/b6f186cc21f79bef0acadd6bf904277ef2f13760))
170 | * wrong index created ([1c6f9ef](https://github.com/QuantGeekDev/mcp-framework/commit/1c6f9ef587f14d77e346df7dbf1b59ad5fb5a3bc))
171 |
172 | ## [Unreleased]
173 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2025] [Alexandr "Andru" Andrushevich]
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 | # MCP Framework
2 |
3 | MCP-Framework is a framework for building Model Context Protocol (MCP) servers elegantly in TypeScript.
4 |
5 | MCP-Framework gives you architecture out of the box, with automatic directory-based discovery for tools, resources, and prompts. Use our powerful MCP abstractions to define tools, resources, or prompts in an elegant way. Our cli makes getting started with your own MCP server a breeze
6 |
7 | ## Features
8 |
9 | - 🛠️ Automatic discovery and loading of tools, resources, and prompts
10 | - Multiple transport support (stdio, SSE, HTTP Stream)
11 | - TypeScript-first development with full type safety
12 | - Built on the official MCP SDK
13 | - Easy-to-use base classes for tools, prompts, and resources
14 | - Out of the box authentication for SSE endpoints
15 |
16 | ## Projects Built with MCP Framework
17 |
18 | The following projects and services are built using MCP Framework:
19 |
20 | - ### [tip.md](https://tip.md)
21 | A crypto tipping service that enables AI assistants to help users send cryptocurrency tips to content creators directly from their chat interface. The MCP service allows for:
22 | - Checking wallet types for users
23 | - Preparing cryptocurrency tips for users/agents to complete
24 | Setup instructions for various clients (Cursor, Sage, Claude Desktop) are available in their [MCP Server documentation](https://docs.tip.md/mcp-server/).
25 |
26 | ## Support our work
27 |
28 | [](https://tip.md/QuantGeekDev)
29 |
30 |
31 | # [Read the full docs here](https://mcp-framework.com)
32 |
33 |
34 |
35 |
36 |
37 | ## Creating a repository with mcp-framework
38 |
39 | ### Using the CLI (Recommended)
40 |
41 | ```bash
42 | # Install the framework globally
43 | npm install -g mcp-framework
44 |
45 | # Create a new MCP server project
46 | mcp create my-mcp-server
47 |
48 | # Navigate to your project
49 | cd my-mcp-server
50 |
51 | # Your server is ready to use!
52 | ```
53 |
54 | ## CLI Usage
55 |
56 | The framework provides a powerful CLI for managing your MCP server projects:
57 |
58 | ### Project Creation
59 |
60 | ```bash
61 | # Create a new project
62 | mcp create
63 |
64 | # Create a new project with the new EXPERIMENTAL HTTP transport
65 | Heads up: This will set cors allowed origin to "*", modify it in the index if you wish
66 | mcp create --http --port 1337 --cors
67 | ```
68 |
69 | # Options:
70 | # --http: Use HTTP transport instead of default stdio
71 | # --port : Specify HTTP port (default: 8080)
72 | # --cors: Enable CORS with wildcard (*) access
73 |
74 | ### Adding a Tool
75 |
76 | ```bash
77 | # Add a new tool
78 | mcp add tool price-fetcher
79 | ```
80 |
81 | ### Adding a Prompt
82 |
83 | ```bash
84 | # Add a new prompt
85 | mcp add prompt price-analysis
86 | ```
87 |
88 | ### Adding a Resource
89 |
90 | ```bash
91 | # Add a new prompt
92 | mcp add resource market-data
93 | ```
94 |
95 | ## Development Workflow
96 |
97 | 1. Create your project:
98 |
99 | ```bash
100 | mcp create my-mcp-server
101 | cd my-mcp-server
102 | ```
103 |
104 | 2. Add tools as needed:
105 |
106 | ```bash
107 | mcp add tool data-fetcher
108 | mcp add tool data-processor
109 | mcp add tool report-generator
110 | ```
111 |
112 | 3. Build:
113 |
114 | ```bash
115 | npm run build
116 |
117 | ```
118 |
119 | 4. Add to MCP Client (Read below for Claude Desktop example)
120 |
121 | ## Using with Claude Desktop
122 |
123 | ### Local Development
124 |
125 | Add this configuration to your Claude Desktop config file:
126 |
127 | **MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
128 | **Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
129 |
130 | ```json
131 | {
132 | "mcpServers": {
133 | "${projectName}": {
134 | "command": "node",
135 | "args":["/absolute/path/to/${projectName}/dist/index.js"]
136 | }
137 | }
138 | }
139 | ```
140 |
141 | ### After Publishing
142 |
143 | Add this configuration to your Claude Desktop config file:
144 |
145 | **MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
146 | **Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
147 |
148 | ```json
149 | {
150 | "mcpServers": {
151 | "${projectName}": {
152 | "command": "npx",
153 | "args": ["${projectName}"]
154 | }
155 | }
156 | }
157 | ```
158 |
159 | ## Building and Testing
160 |
161 | 1. Make changes to your tools
162 | 2. Run \`npm run build\` to compile
163 | 3. The server will automatically load your tools on startup
164 |
165 | ## Environment Variables
166 |
167 | The framework supports the following environment variables for configuration:
168 |
169 | | Variable | Description | Default |
170 | |-----------------------|-------------------------------------------------------|-------------|
171 | | MCP_ENABLE_FILE_LOGGING | Enable logging to files (true/false) | false |
172 | | MCP_LOG_DIRECTORY | Directory where log files will be stored | logs |
173 | | MCP_DEBUG_CONSOLE | Display debug level messages in console (true/false) | false |
174 |
175 | Example usage:
176 |
177 | ```bash
178 | # Enable file logging
179 | MCP_ENABLE_FILE_LOGGING=true node dist/index.js
180 |
181 | # Specify a custom log directory
182 | MCP_ENABLE_FILE_LOGGING=true MCP_LOG_DIRECTORY=my-logs
183 | # Enable debug messages in console
184 | MCP_DEBUG_CONSOLE=true```
185 |
186 | ## Quick Start
187 |
188 | ### Creating a Tool
189 |
190 | ```typescript
191 | import { MCPTool } from "mcp-framework";
192 | import { z } from "zod";
193 |
194 | interface ExampleInput {
195 | message: string;
196 | }
197 |
198 | class ExampleTool extends MCPTool {
199 | name = "example_tool";
200 | description = "An example tool that processes messages";
201 |
202 | schema = {
203 | message: {
204 | type: z.string(),
205 | description: "Message to process",
206 | },
207 | };
208 |
209 | async execute(input: ExampleInput) {
210 | return `Processed: ${input.message}`;
211 | }
212 | }
213 |
214 | export default ExampleTool;
215 | ```
216 |
217 | ### Setting up the Server
218 |
219 | ```typescript
220 | import { MCPServer } from "mcp-framework";
221 |
222 | const server = new MCPServer();
223 |
224 | // OR (mutually exclusive!) with SSE transport
225 | const server = new MCPServer({
226 | transport: {
227 | type: "sse",
228 | options: {
229 | port: 8080 // Optional (default: 8080)
230 | }
231 | }
232 | });
233 |
234 | // Start the server
235 | await server.start();
236 | ```
237 |
238 | ## Transport Configuration
239 |
240 | ### stdio Transport (Default)
241 |
242 | The stdio transport is used by default if no transport configuration is provided:
243 |
244 | ```typescript
245 | const server = new MCPServer();
246 | // or explicitly:
247 | const server = new MCPServer({
248 | transport: { type: "stdio" }
249 | });
250 | ```
251 |
252 | ### SSE Transport
253 |
254 | To use Server-Sent Events (SSE) transport:
255 |
256 | ```typescript
257 | const server = new MCPServer({
258 | transport: {
259 | type: "sse",
260 | options: {
261 | port: 8080, // Optional (default: 8080)
262 | endpoint: "/sse", // Optional (default: "/sse")
263 | messageEndpoint: "/messages", // Optional (default: "/messages")
264 | cors: {
265 | allowOrigin: "*", // Optional (default: "*")
266 | allowMethods: "GET, POST, OPTIONS", // Optional (default: "GET, POST, OPTIONS")
267 | allowHeaders: "Content-Type, Authorization, x-api-key", // Optional (default: "Content-Type, Authorization, x-api-key")
268 | exposeHeaders: "Content-Type, Authorization, x-api-key", // Optional (default: "Content-Type, Authorization, x-api-key")
269 | maxAge: "86400" // Optional (default: "86400")
270 | }
271 | }
272 | }
273 | });
274 | ```
275 |
276 | ### HTTP Stream Transport
277 |
278 | To use HTTP Stream transport:
279 |
280 | ```typescript
281 | const server = new MCPServer({
282 | transport: {
283 | type: "http-stream",
284 | options: {
285 | port: 8080, // Optional (default: 8080)
286 | endpoint: "/mcp", // Optional (default: "/mcp")
287 | responseMode: "batch", // Optional (default: "batch"), can be "batch" or "stream"
288 | batchTimeout: 30000, // Optional (default: 30000ms) - timeout for batch responses
289 | maxMessageSize: "4mb", // Optional (default: "4mb") - maximum message size
290 |
291 | // Session configuration
292 | session: {
293 | enabled: true, // Optional (default: true)
294 | headerName: "Mcp-Session-Id", // Optional (default: "Mcp-Session-Id")
295 | allowClientTermination: true, // Optional (default: true)
296 | },
297 |
298 | // Stream resumability (for missed messages)
299 | resumability: {
300 | enabled: false, // Optional (default: false)
301 | historyDuration: 300000, // Optional (default: 300000ms = 5min) - how long to keep message history
302 | },
303 |
304 | // CORS configuration
305 | cors: {
306 | allowOrigin: "*" // Other CORS options use defaults
307 | }
308 | }
309 | }
310 | });
311 | ```
312 |
313 | #### Response Modes
314 |
315 | The HTTP Stream transport supports two response modes:
316 |
317 | 1. **Batch Mode** (Default): Responses are collected and sent as a single JSON-RPC response. This is suitable for typical request-response patterns and is more efficient for most use cases.
318 |
319 | 2. **Stream Mode**: All responses are sent over a persistent SSE connection opened for each request. This is ideal for long-running operations or when the server needs to send multiple messages in response to a single request.
320 |
321 | You can configure the response mode based on your specific needs:
322 |
323 | ```typescript
324 | // For batch mode (default):
325 | const server = new MCPServer({
326 | transport: {
327 | type: "http-stream",
328 | options: {
329 | responseMode: "batch"
330 | }
331 | }
332 | });
333 |
334 | // For stream mode:
335 | const server = new MCPServer({
336 | transport: {
337 | type: "http-stream",
338 | options: {
339 | responseMode: "stream"
340 | }
341 | }
342 | });
343 | ```
344 |
345 | #### HTTP Stream Transport Features
346 |
347 | - **Session Management**: Automatic session tracking and management
348 | - **Stream Resumability**: Optional support for resuming streams after connection loss
349 | - **Batch Processing**: Support for JSON-RPC batch requests/responses
350 | - **Comprehensive Error Handling**: Detailed error responses with JSON-RPC error codes
351 |
352 | ## Authentication
353 |
354 | MCP Framework provides optional authentication for SSE endpoints. You can choose between JWT and API Key authentication, or implement your own custom authentication provider.
355 |
356 | ### JWT Authentication
357 |
358 | ```typescript
359 | import { MCPServer, JWTAuthProvider } from "mcp-framework";
360 | import { Algorithm } from "jsonwebtoken";
361 |
362 | const server = new MCPServer({
363 | transport: {
364 | type: "sse",
365 | options: {
366 | auth: {
367 | provider: new JWTAuthProvider({
368 | secret: process.env.JWT_SECRET,
369 | algorithms: ["HS256" as Algorithm], // Optional (default: ["HS256"])
370 | headerName: "Authorization" // Optional (default: "Authorization")
371 | }),
372 | endpoints: {
373 | sse: true, // Protect SSE endpoint (default: false)
374 | messages: true // Protect message endpoint (default: true)
375 | }
376 | }
377 | }
378 | }
379 | });
380 | ```
381 |
382 | Clients must include a valid JWT token in the Authorization header:
383 | ```
384 | Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
385 | ```
386 |
387 | ### API Key Authentication
388 |
389 | ```typescript
390 | import { MCPServer, APIKeyAuthProvider } from "mcp-framework";
391 |
392 | const server = new MCPServer({
393 | transport: {
394 | type: "sse",
395 | options: {
396 | auth: {
397 | provider: new APIKeyAuthProvider({
398 | keys: [process.env.API_KEY],
399 | headerName: "X-API-Key" // Optional (default: "X-API-Key")
400 | })
401 | }
402 | }
403 | }
404 | });
405 | ```
406 |
407 | Clients must include a valid API key in the X-API-Key header:
408 | ```
409 | X-API-Key: your-api-key
410 | ```
411 |
412 | ### Custom Authentication
413 |
414 | You can implement your own authentication provider by implementing the `AuthProvider` interface:
415 |
416 | ```typescript
417 | import { AuthProvider, AuthResult } from "mcp-framework";
418 | import { IncomingMessage } from "node:http";
419 |
420 | class CustomAuthProvider implements AuthProvider {
421 | async authenticate(req: IncomingMessage): Promise {
422 | // Implement your custom authentication logic
423 | return true;
424 | }
425 |
426 | getAuthError() {
427 | return {
428 | status: 401,
429 | message: "Authentication failed"
430 | };
431 | }
432 | }
433 | ```
434 |
435 | ## License
436 |
437 | MIT
438 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "eslint/config";
2 | import globals from "globals";
3 | import js from "@eslint/js";
4 | import tseslint from "typescript-eslint";
5 |
6 |
7 | export default defineConfig([
8 | { files: ["**/*.{js,mjs,cjs,ts}"] },
9 | { files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.node } },
10 | { files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] },
11 | tseslint.configs.recommended,
12 | {
13 | files: ["**/*.{js,mjs,cjs,ts}"],
14 | rules: {
15 | "@typescript-eslint/no-unused-vars": "off",
16 | "@typescript-eslint/no-explicit-any": "off",
17 | "@typescript-eslint/no-empty-object-type": "off",
18 | },
19 | },
20 | ]);
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mcp-framework",
3 | "version": "0.2.13",
4 | "description": "Framework for building Model Context Protocol (MCP) servers in Typescript",
5 | "type": "module",
6 | "author": "Alex Andru ",
7 | "main": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "types": "./dist/index.d.ts",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "bin": {
19 | "mcp": "dist/cli/index.js",
20 | "mcp-build": "dist/cli/framework/build-cli.js"
21 | },
22 | "scripts": {
23 | "build": "tsc",
24 | "watch": "tsc --watch",
25 | "lint": "eslint",
26 | "lint:fix": "eslint --fix",
27 | "format": "prettier --write \"src/**/*.ts\"",
28 | "prepare": "npm run build"
29 | "dev:pub": "rm -rf dist && npm run build && yalc publish --push"
30 | },
31 | "engines": {
32 | "node": ">=18.19.0"
33 | },
34 | "keywords": [
35 | "mcp",
36 | "claude",
37 | "anthropic",
38 | "ai",
39 | "framework",
40 | "tools",
41 | "modelcontextprotocol",
42 | "model",
43 | "context",
44 | "protocol"
45 | ],
46 | "peerDependencies": {
47 | "@modelcontextprotocol/sdk": "^1.11.0"
48 | },
49 | "dependencies": {
50 | "@types/prompts": "^2.4.9",
51 | "commander": "^12.1.0",
52 | "content-type": "^1.0.5",
53 | "execa": "^9.5.2",
54 | "find-up": "^7.0.0",
55 | "jsonwebtoken": "^9.0.2",
56 | "prompts": "^2.4.2",
57 | "raw-body": "^2.5.2",
58 | "typescript": "^5.3.3",
59 | "zod": "^3.23.8"
60 | },
61 | "devDependencies": {
62 | "@eslint/js": "^9.23.0",
63 | "@types/content-type": "^1.1.8",
64 | "@types/jest": "^29.5.12",
65 | "@types/jsonwebtoken": "^9.0.8",
66 | "@types/node": "^20.17.28",
67 | "@typescript-eslint/eslint-plugin": "^8.28.0",
68 | "@typescript-eslint/parser": "^8.28.0",
69 | "eslint": "^9.23.0",
70 | "eslint-config-prettier": "^10.1.1",
71 | "eslint-plugin-prettier": "^5.2.5",
72 | "globals": "^16.0.0",
73 | "jest": "^29.7.0",
74 | "prettier": "^3.5.3",
75 | "ts-jest": "^29.1.2",
76 | "typescript-eslint": "^8.28.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./types.js";
2 | export * from "./providers/jwt.js";
3 | export * from "./providers/apikey.js";
4 |
5 | export type { AuthProvider, AuthConfig, AuthResult } from "./types.js";
6 | export type { JWTConfig } from "./providers/jwt.js";
7 | export type { APIKeyConfig } from "./providers/apikey.js";
8 |
--------------------------------------------------------------------------------
/src/auth/providers/apikey.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage } from "node:http";
2 | import { logger } from "../../core/Logger.js";
3 | import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";
4 |
5 | export const DEFAULT_API_KEY_HEADER_NAME = "X-API-Key"
6 | /**
7 | * Configuration options for API key authentication
8 | */
9 | export interface APIKeyConfig {
10 | /**
11 | * Valid API keys
12 | */
13 | keys: string[];
14 |
15 | /**
16 | * Name of the header containing the API key
17 | * @default "X-API-Key"
18 | */
19 | headerName?: string;
20 | }
21 |
22 | /**
23 | * API key-based authentication provider
24 | */
25 | export class APIKeyAuthProvider implements AuthProvider {
26 | private config: Required;
27 |
28 | constructor(config: APIKeyConfig) {
29 | this.config = {
30 | headerName: DEFAULT_API_KEY_HEADER_NAME,
31 | ...config
32 | };
33 |
34 | if (!this.config.keys?.length) {
35 | throw new Error("At least one API key is required");
36 | }
37 | }
38 |
39 | /**
40 | * Get the number of configured API keys
41 | */
42 | getKeyCount(): number {
43 | return this.config.keys.length;
44 | }
45 |
46 | /**
47 | * Get the configured header name
48 | */
49 | getHeaderName(): string {
50 | return this.config.headerName;
51 | }
52 |
53 | async authenticate(req: IncomingMessage): Promise {
54 | logger.debug(`API Key auth attempt from ${req.socket.remoteAddress}`);
55 |
56 | logger.debug(`All request headers: ${JSON.stringify(req.headers, null, 2)}`);
57 |
58 | const headerVariations = [
59 | this.config.headerName,
60 | this.config.headerName.toLowerCase(),
61 | this.config.headerName.toUpperCase(),
62 | 'x-api-key',
63 | 'X-API-KEY',
64 | 'X-Api-Key'
65 | ];
66 |
67 | logger.debug(`Looking for header variations: ${headerVariations.join(', ')}`);
68 |
69 | let apiKey: string | undefined;
70 | let matchedHeader: string | undefined;
71 |
72 | for (const [key, value] of Object.entries(req.headers)) {
73 | const lowerKey = key.toLowerCase();
74 | if (headerVariations.some(h => h.toLowerCase() === lowerKey)) {
75 | apiKey = Array.isArray(value) ? value[0] : value;
76 | matchedHeader = key;
77 | break;
78 | }
79 | }
80 |
81 | if (!apiKey) {
82 | logger.debug(`API Key header missing}`);
83 | logger.debug(`Available headers: ${Object.keys(req.headers).join(', ')}`);
84 | return false;
85 | }
86 |
87 | logger.debug(`Found API key in header: ${matchedHeader}`);
88 |
89 | for (const validKey of this.config.keys) {
90 | if (apiKey === validKey) {
91 | logger.debug(`API Key authentication successful`);
92 | return true;
93 | }
94 | }
95 |
96 | logger.debug(`Invalid API Key provided`);
97 | return false;
98 | }
99 |
100 | getAuthError() {
101 | return {
102 | ...DEFAULT_AUTH_ERROR,
103 | message: "Invalid API key"
104 | };
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/auth/providers/jwt.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage } from "node:http";
2 | import jwt, { Algorithm } from "jsonwebtoken";
3 | import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";
4 | import { logger } from "../../core/Logger.js";
5 |
6 | /**
7 | * Configuration options for JWT authentication
8 | */
9 | export interface JWTConfig {
10 | /**
11 | * Secret key for verifying JWT tokens
12 | */
13 | secret: string;
14 |
15 | /**
16 | * Allowed JWT algorithms
17 | * @default ["HS256"]
18 | */
19 | algorithms?: Algorithm[];
20 |
21 | /**
22 | * Name of the header containing the JWT token
23 | * @default "Authorization"
24 | */
25 | headerName?: string;
26 |
27 | /**
28 | * Whether to require "Bearer" prefix in Authorization header
29 | * @default true
30 | */
31 | requireBearer?: boolean;
32 | }
33 |
34 | /**
35 | * JWT-based authentication provider
36 | */
37 | export class JWTAuthProvider implements AuthProvider {
38 | private config: Required;
39 |
40 | constructor(config: JWTConfig) {
41 | this.config = {
42 | algorithms: ["HS256"],
43 | headerName: "Authorization",
44 | requireBearer: true,
45 | ...config
46 | };
47 |
48 | if (!this.config.secret) {
49 | throw new Error("JWT secret is required");
50 | }
51 | }
52 |
53 | async authenticate(req: IncomingMessage): Promise {
54 | const authHeader = req.headers[this.config.headerName.toLowerCase()];
55 |
56 | if (!authHeader || typeof authHeader !== "string") {
57 | return false;
58 | }
59 |
60 | let token = authHeader;
61 | if (this.config.requireBearer) {
62 | if (!authHeader.startsWith("Bearer ")) {
63 | return false;
64 | }
65 | token = authHeader.split(" ")[1];
66 | }
67 |
68 | try {
69 | const decoded = jwt.verify(token, this.config.secret, {
70 | algorithms: this.config.algorithms
71 | });
72 |
73 | return {
74 | data: typeof decoded === "object" ? decoded : { sub: decoded }
75 | };
76 | } catch (error) {
77 | logger.debug(`JWT verification failed: ${(error as Error).message}`);
78 | return false;
79 | }
80 | }
81 |
82 | getAuthError() {
83 | return {
84 | ...DEFAULT_AUTH_ERROR,
85 | message: "Invalid or expired JWT token"
86 | };
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/auth/types.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage } from "node:http";
2 |
3 | /**
4 | * Result of successful authentication
5 | */
6 | export interface AuthResult {
7 | /**
8 | * User or token data from authentication
9 | */
10 | data?: Record;
11 | }
12 |
13 | /**
14 | * Base interface for authentication providers
15 | */
16 | export interface AuthProvider {
17 | /**
18 | * Authenticate an incoming request
19 | * @param req The incoming HTTP request
20 | * @returns Promise resolving to boolean or AuthResult
21 | */
22 | authenticate(req: IncomingMessage): Promise;
23 |
24 | /**
25 | * Get error details for failed authentication
26 | */
27 | getAuthError?(): { status: number; message: string };
28 | }
29 |
30 | /**
31 | * Authentication configuration for transport
32 | */
33 | export interface AuthConfig {
34 | /**
35 | * Authentication provider implementation
36 | */
37 | provider: AuthProvider;
38 |
39 | /**
40 | * Per-endpoint authentication configuration
41 | */
42 | endpoints?: {
43 | /**
44 | * Whether to authenticate SSE connection endpoint
45 | * @default false
46 | */
47 | sse?: boolean;
48 |
49 | /**
50 | * Whether to authenticate message endpoint
51 | * @default true
52 | */
53 | messages?: boolean;
54 | };
55 | }
56 |
57 | /**
58 | * Default authentication error
59 | */
60 | export const DEFAULT_AUTH_ERROR = {
61 | status: 401,
62 | message: "Unauthorized"
63 | };
64 |
--------------------------------------------------------------------------------
/src/cli/framework/build-cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { buildFramework } from './build.js';
3 |
4 | if (process.argv[1].endsWith('mcp-build') || process.argv[1].endsWith('build-cli.js')) {
5 | buildFramework().catch(error => {
6 | process.stderr.write(`Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
7 | process.exit(1);
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/cli/framework/build.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { execa } from "execa";
3 | import { readFile, writeFile } from "fs/promises";
4 | import { join, dirname } from "path";
5 | import { findUp } from 'find-up';
6 |
7 | export async function buildFramework() {
8 | process.stderr.write("MCP Build Script Starting...\n");
9 | process.stderr.write("Finding project root...\n");
10 |
11 | const startDir = process.cwd();
12 | process.stderr.write(`Starting search from: ${startDir}\n`);
13 |
14 | const skipValidation = process.env.MCP_SKIP_VALIDATION === 'true';
15 | if (skipValidation) {
16 | process.stderr.write(`Skipping dependency validation\n`);
17 | }
18 |
19 | try {
20 | const pkgPath = await findUp('package.json');
21 | if (!pkgPath) {
22 | throw new Error("Could not find package.json in current directory or any parent directories");
23 | }
24 |
25 | const projectRoot = dirname(pkgPath);
26 |
27 | if (!skipValidation) {
28 | const pkgContent = await readFile(pkgPath, 'utf8');
29 | const pkg = JSON.parse(pkgContent);
30 |
31 | if (!pkg.dependencies?.["mcp-framework"]) {
32 | throw new Error("This directory is not an MCP project (mcp-framework not found in dependencies)");
33 | }
34 | }
35 |
36 | process.stderr.write(`Running tsc in ${projectRoot}\n`);
37 |
38 | const tscCommand = process.platform === 'win32' ? ['npx.cmd', 'tsc'] : ['npx', 'tsc'];
39 |
40 | await execa(tscCommand[0], [tscCommand[1]], {
41 | cwd: projectRoot,
42 | stdio: "inherit",
43 | env: {
44 | ...process.env,
45 | ELECTRON_RUN_AS_NODE: "1",
46 | FORCE_COLOR: "1"
47 | }
48 | });
49 |
50 | const distPath = join(projectRoot, "dist");
51 | const projectIndexPath = join(distPath, "index.js");
52 | const shebang = "#!/usr/bin/env node\n";
53 |
54 | process.stderr.write("Adding shebang to index.js...\n");
55 | try {
56 | const content = await readFile(projectIndexPath, "utf8");
57 | if (!content.startsWith(shebang)) {
58 | await writeFile(projectIndexPath, shebang + content);
59 | }
60 | } catch (error) {
61 | process.stderr.write(`Error processing index.js: ${error instanceof Error ? error.message : String(error)}\n`);
62 | throw error;
63 | }
64 |
65 | process.stderr.write("Build completed successfully!\n");
66 | } catch (error) {
67 | process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
68 | process.exit(1);
69 | }
70 | }
71 |
72 | export default buildFramework;
73 |
--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { Command } from "commander";
3 | import { createProject } from "./project/create.js";
4 | import { addTool } from "./project/add-tool.js";
5 | import { addPrompt } from "./project/add-prompt.js";
6 | import { addResource } from "./project/add-resource.js";
7 | import { buildFramework } from "./framework/build.js";
8 |
9 | const program = new Command();
10 |
11 | program
12 | .name("mcp")
13 | .description("CLI for managing MCP server projects")
14 | .version("0.2.2");
15 |
16 | program
17 | .command("build")
18 | .description("Build the MCP project")
19 | .action(buildFramework);
20 |
21 | program
22 | .command("create")
23 | .description("Create a new MCP server project")
24 | .argument("[name]", "project name")
25 | .option("--http", "use HTTP transport instead of default stdio")
26 | .option("--cors", "enable CORS with wildcard (*) access")
27 | .option("--port ", "specify HTTP port (only valid with --http)", (val) => parseInt(val, 10))
28 | .option("--no-install", "skip npm install and build steps")
29 | .option("--no-example", "skip creating example tool")
30 | .action(createProject);
31 |
32 | program
33 | .command("add")
34 | .description("Add a new component to your MCP server")
35 | .addCommand(
36 | new Command("tool")
37 | .description("Add a new tool")
38 | .argument("[name]", "tool name")
39 | .action(addTool)
40 | )
41 | .addCommand(
42 | new Command("prompt")
43 | .description("Add a new prompt")
44 | .argument("[name]", "prompt name")
45 | .action(addPrompt)
46 | )
47 | .addCommand(
48 | new Command("resource")
49 | .description("Add a new resource")
50 | .argument("[name]", "resource name")
51 | .action(addResource)
52 | );
53 |
54 | program.parse();
55 |
--------------------------------------------------------------------------------
/src/cli/project/add-prompt.ts:
--------------------------------------------------------------------------------
1 | import { writeFile, mkdir } from "fs/promises";
2 | import { join } from "path";
3 | import prompts from "prompts";
4 | import { toPascalCase } from "../utils/string-utils.js";
5 | import { validateMCPProject } from "../utils/validate-project.js";
6 |
7 | export async function addPrompt(name?: string) {
8 | await validateMCPProject();
9 |
10 | let promptName = name;
11 | if (!promptName) {
12 | const response = await prompts([
13 | {
14 | type: "text",
15 | name: "name",
16 | message: "What is the name of your prompt?",
17 | validate: (value: string) =>
18 | /^[a-z0-9-]+$/.test(value)
19 | ? true
20 | : "Prompt name can only contain lowercase letters, numbers, and hyphens",
21 | },
22 | ]);
23 |
24 | if (!response.name) {
25 | console.log("Prompt creation cancelled");
26 | process.exit(1);
27 | }
28 |
29 | promptName = response.name;
30 | }
31 |
32 | if (!promptName) {
33 | throw new Error("Prompt name is required");
34 | }
35 |
36 | const className = toPascalCase(promptName);
37 | const fileName = `${className}Prompt.ts`;
38 | const promptsDir = join(process.cwd(), "src/prompts");
39 |
40 | try {
41 | await mkdir(promptsDir, { recursive: true });
42 |
43 | const promptContent = `import { MCPPrompt } from "mcp-framework";
44 | import { z } from "zod";
45 |
46 | interface ${className}Input {
47 | message: string;
48 | }
49 |
50 | class ${className}Prompt extends MCPPrompt<${className}Input> {
51 | name = "${promptName}";
52 | description = "${className} prompt description";
53 |
54 | schema = {
55 | message: {
56 | type: z.string(),
57 | description: "Message to process",
58 | required: true,
59 | },
60 | };
61 |
62 | async generateMessages({ message }: ${className}Input) {
63 | return [
64 | {
65 | role: "user",
66 | content: {
67 | type: "text",
68 | text: message,
69 | },
70 | },
71 | ];
72 | }
73 | }
74 |
75 | export default ${className}Prompt;`;
76 |
77 | await writeFile(join(promptsDir, fileName), promptContent);
78 |
79 | console.log(
80 | `Prompt ${promptName} created successfully at src/prompts/${fileName}`
81 | );
82 | } catch (error) {
83 | console.error("Error creating prompt:", error);
84 | process.exit(1);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/cli/project/add-resource.ts:
--------------------------------------------------------------------------------
1 | import { writeFile, mkdir } from "fs/promises";
2 | import { join } from "path";
3 | import prompts from "prompts";
4 | import { validateMCPProject } from "../utils/validate-project.js";
5 | import { toPascalCase } from "../utils/string-utils.js";
6 |
7 | export async function addResource(name?: string) {
8 | await validateMCPProject();
9 |
10 | let resourceName = name;
11 | if (!resourceName) {
12 | const response = await prompts([
13 | {
14 | type: "text",
15 | name: "name",
16 | message: "What is the name of your resource?",
17 | validate: (value: string) =>
18 | /^[a-z0-9-]+$/.test(value)
19 | ? true
20 | : "Resource name can only contain lowercase letters, numbers, and hyphens",
21 | },
22 | ]);
23 |
24 | if (!response.name) {
25 | console.log("Resource creation cancelled");
26 | process.exit(1);
27 | }
28 |
29 | resourceName = response.name;
30 | }
31 |
32 | if (!resourceName) {
33 | throw new Error("Resource name is required");
34 | }
35 |
36 | const className = toPascalCase(resourceName);
37 | const fileName = `${className}Resource.ts`;
38 | const resourcesDir = join(process.cwd(), "src/resources");
39 |
40 | try {
41 | await mkdir(resourcesDir, { recursive: true });
42 |
43 | const resourceContent = `import { MCPResource, ResourceContent } from "mcp-framework";
44 |
45 | class ${className}Resource extends MCPResource {
46 | uri = "resource://${resourceName}";
47 | name = "${className}";
48 | description = "${className} resource description";
49 | mimeType = "application/json";
50 |
51 | async read(): Promise {
52 | return [
53 | {
54 | uri: this.uri,
55 | mimeType: this.mimeType,
56 | text: JSON.stringify({ message: "Hello from ${className} resource" }),
57 | },
58 | ];
59 | }
60 | }
61 |
62 | export default ${className}Resource;`;
63 |
64 | await writeFile(join(resourcesDir, fileName), resourceContent);
65 |
66 | console.log(
67 | `Resource ${resourceName} created successfully at src/resources/${fileName}`
68 | );
69 | } catch (error) {
70 | console.error("Error creating resource:", error);
71 | process.exit(1);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/cli/project/add-tool.ts:
--------------------------------------------------------------------------------
1 | import { writeFile, mkdir } from "fs/promises";
2 | import { join } from "path";
3 | import prompts from "prompts";
4 | import { validateMCPProject } from "../utils/validate-project.js";
5 | import { toPascalCase } from "../utils/string-utils.js";
6 |
7 | export async function addTool(name?: string) {
8 | await validateMCPProject();
9 |
10 | let toolName = name;
11 | if (!toolName) {
12 | const response = await prompts([
13 | {
14 | type: "text",
15 | name: "name",
16 | message: "What is the name of your tool?",
17 | validate: (value: string) =>
18 | /^[a-z0-9-]+$/.test(value)
19 | ? true
20 | : "Tool name can only contain lowercase letters, numbers, and hyphens",
21 | },
22 | ]);
23 |
24 | if (!response.name) {
25 | console.log("Tool creation cancelled");
26 | process.exit(1);
27 | }
28 |
29 | toolName = response.name;
30 | }
31 |
32 | if (!toolName) {
33 | throw new Error("Tool name is required");
34 | }
35 |
36 | const className = toPascalCase(toolName);
37 | const fileName = `${className}Tool.ts`;
38 | const toolsDir = join(process.cwd(), "src/tools");
39 |
40 | try {
41 | await mkdir(toolsDir, { recursive: true });
42 |
43 | const toolContent = `import { MCPTool } from "mcp-framework";
44 | import { z } from "zod";
45 |
46 | interface ${className}Input {
47 | message: string;
48 | }
49 |
50 | class ${className}Tool extends MCPTool<${className}Input> {
51 | name = "${toolName}";
52 | description = "${className} tool description";
53 |
54 | schema = {
55 | message: {
56 | type: z.string(),
57 | description: "Message to process",
58 | },
59 | };
60 |
61 | async execute(input: ${className}Input) {
62 | return \`Processed: \${input.message}\`;
63 | }
64 | }
65 |
66 | export default ${className}Tool;`;
67 |
68 | await writeFile(join(toolsDir, fileName), toolContent);
69 |
70 | console.log(
71 | `Tool ${toolName} created successfully at src/tools/${fileName}`
72 | );
73 | } catch (error) {
74 | console.error("Error creating tool:", error);
75 | process.exit(1);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/cli/project/create.ts:
--------------------------------------------------------------------------------
1 | import { spawnSync } from "child_process";
2 | import { mkdir, writeFile } from "fs/promises";
3 | import { join } from "path";
4 | import prompts from "prompts";
5 | import { generateReadme } from "../templates/readme.js";
6 | import { execa } from "execa";
7 |
8 | export async function createProject(name?: string, options?: { http?: boolean, cors?: boolean, port?: number, install?: boolean, example?: boolean }) {
9 | let projectName: string;
10 | // Default install and example to true if not specified
11 | const shouldInstall = options?.install !== false;
12 | const shouldCreateExample = options?.example !== false;
13 |
14 | if (!name) {
15 | const response = await prompts([
16 | {
17 | type: "text",
18 | name: "projectName",
19 | message: "What is the name of your MCP server project?",
20 | validate: (value: string) =>
21 | /^[a-z0-9-]+$/.test(value)
22 | ? true
23 | : "Project name can only contain lowercase letters, numbers, and hyphens",
24 | },
25 | ]);
26 |
27 | if (!response.projectName) {
28 | console.log("Project creation cancelled");
29 | process.exit(1);
30 | }
31 |
32 | projectName = response.projectName as string;
33 | } else {
34 | projectName = name;
35 | }
36 |
37 | if (!projectName) {
38 | throw new Error("Project name is required");
39 | }
40 |
41 | const projectDir = join(process.cwd(), projectName);
42 | const srcDir = join(projectDir, "src");
43 | const toolsDir = join(srcDir, "tools");
44 |
45 | try {
46 | console.log("Creating project structure...");
47 | await mkdir(projectDir);
48 | await mkdir(srcDir);
49 | await mkdir(toolsDir);
50 |
51 | const packageJson = {
52 | name: projectName,
53 | version: "0.0.1",
54 | description: `${projectName} MCP server`,
55 | type: "module",
56 | bin: {
57 | [projectName]: "./dist/index.js",
58 | },
59 | files: ["dist"],
60 | scripts: {
61 | build: "tsc && mcp-build",
62 | watch: "tsc --watch",
63 | start: "node dist/index.js"
64 | },
65 | dependencies: {
66 | "mcp-framework": "^0.2.2"
67 | },
68 | devDependencies: {
69 | "@types/node": "^20.11.24",
70 | "typescript": "^5.3.3"
71 | },
72 | engines: {
73 | "node": ">=18.19.0"
74 | }
75 | };
76 |
77 | const tsconfig = {
78 | compilerOptions: {
79 | target: "ESNext",
80 | module: "ESNext",
81 | moduleResolution: "node",
82 | outDir: "./dist",
83 | rootDir: "./src",
84 | strict: true,
85 | esModuleInterop: true,
86 | skipLibCheck: true,
87 | forceConsistentCasingInFileNames: true,
88 | },
89 | include: ["src/**/*"],
90 | exclude: ["node_modules"],
91 | };
92 |
93 | let indexTs = "";
94 |
95 | if (options?.http) {
96 | const port = options.port || 8080;
97 | let transportConfig = `\n transport: {
98 | type: "http-stream",
99 | options: {
100 | port: ${port}`;
101 |
102 | if (options.cors) {
103 | transportConfig += `,
104 | cors: {
105 | allowOrigin: "*"
106 | }`;
107 | }
108 |
109 | transportConfig += `
110 | }
111 | }`;
112 |
113 | indexTs = `import { MCPServer } from "mcp-framework";
114 |
115 | const server = new MCPServer({${transportConfig}});
116 |
117 | server.start();`;
118 | } else {
119 | indexTs = `import { MCPServer } from "mcp-framework";
120 |
121 | const server = new MCPServer();
122 |
123 | server.start();`;
124 | }
125 |
126 | const exampleToolTs = `import { MCPTool } from "mcp-framework";
127 | import { z } from "zod";
128 |
129 | interface ExampleInput {
130 | message: string;
131 | }
132 |
133 | class ExampleTool extends MCPTool {
134 | name = "example_tool";
135 | description = "An example tool that processes messages";
136 |
137 | schema = {
138 | message: {
139 | type: z.string(),
140 | description: "Message to process",
141 | },
142 | };
143 |
144 | async execute(input: ExampleInput) {
145 | return \`Processed: \${input.message}\`;
146 | }
147 | }
148 |
149 | export default ExampleTool;`;
150 |
151 | // Prepare the files to write
152 | const filesToWrite = [
153 | writeFile(join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2)),
154 | writeFile(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2)),
155 | writeFile(join(projectDir, "README.md"), generateReadme(projectName)),
156 | writeFile(join(srcDir, "index.ts"), indexTs),
157 | ];
158 |
159 | // Conditionally add the example tool
160 | if (shouldCreateExample) {
161 | filesToWrite.push(writeFile(join(toolsDir, "ExampleTool.ts"), exampleToolTs));
162 | }
163 |
164 | console.log("Creating project files...");
165 | await Promise.all(filesToWrite);
166 |
167 | process.chdir(projectDir);
168 |
169 | console.log("Initializing git repository...");
170 | const gitInit = spawnSync("git", ["init"], {
171 | stdio: "inherit",
172 | shell: true,
173 | });
174 |
175 | if (gitInit.status !== 0) {
176 | throw new Error("Failed to initialize git repository");
177 | }
178 |
179 | if (shouldInstall) {
180 | console.log("Installing dependencies...");
181 | const npmInstall = spawnSync("npm", ["install"], {
182 | stdio: "inherit",
183 | shell: true
184 | });
185 |
186 | if (npmInstall.status !== 0) {
187 | throw new Error("Failed to install dependencies");
188 | }
189 |
190 | console.log("Building project...");
191 | const tscBuild = await execa('npx', ['tsc'], {
192 | cwd: projectDir,
193 | stdio: "inherit",
194 | });
195 |
196 | if (tscBuild.exitCode !== 0) {
197 | throw new Error("Failed to build TypeScript");
198 | }
199 |
200 | const mcpBuild = await execa('npx', ['mcp-build'], {
201 | cwd: projectDir,
202 | stdio: "inherit",
203 | env: {
204 | ...process.env,
205 | MCP_SKIP_VALIDATION: "true"
206 | }
207 | });
208 |
209 | if (mcpBuild.exitCode !== 0) {
210 | throw new Error("Failed to run mcp-build");
211 | }
212 |
213 | console.log(`
214 | Project ${projectName} created and built successfully!
215 |
216 | You can now:
217 | 1. cd ${projectName}
218 | 2. Add more tools using:
219 | mcp add tool
220 | `);
221 | } else {
222 | console.log(`
223 | Project ${projectName} created successfully (without dependencies)!
224 |
225 | You can now:
226 | 1. cd ${projectName}
227 | 2. Run 'npm install' to install dependencies
228 | 3. Run 'npm run build' to build the project
229 | 4. Add more tools using:
230 | mcp add tool
231 | `);
232 | }
233 | } catch (error) {
234 | console.error("Error creating project:", error);
235 | process.exit(1);
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/cli/templates/readme.ts:
--------------------------------------------------------------------------------
1 | export function generateReadme(projectName: string): string {
2 | return `# ${projectName}
3 |
4 | A Model Context Protocol (MCP) server built with mcp-framework.
5 |
6 | ## Quick Start
7 |
8 | \`\`\`bash
9 | # Install dependencies
10 | npm install
11 |
12 | # Build the project
13 | npm run build
14 |
15 | \`\`\`
16 |
17 | ## Project Structure
18 |
19 | \`\`\`
20 | ${projectName}/
21 | ├── src/
22 | │ ├── tools/ # MCP Tools
23 | │ │ └── ExampleTool.ts
24 | │ └── index.ts # Server entry point
25 | ├── package.json
26 | └── tsconfig.json
27 | \`\`\`
28 |
29 | ## Adding Components
30 |
31 | The project comes with an example tool in \`src/tools/ExampleTool.ts\`. You can add more tools using the CLI:
32 |
33 | \`\`\`bash
34 | # Add a new tool
35 | mcp add tool my-tool
36 |
37 | # Example tools you might create:
38 | mcp add tool data-processor
39 | mcp add tool api-client
40 | mcp add tool file-handler
41 | \`\`\`
42 |
43 | ## Tool Development
44 |
45 | Example tool structure:
46 |
47 | \`\`\`typescript
48 | import { MCPTool } from "mcp-framework";
49 | import { z } from "zod";
50 |
51 | interface MyToolInput {
52 | message: string;
53 | }
54 |
55 | class MyTool extends MCPTool {
56 | name = "my_tool";
57 | description = "Describes what your tool does";
58 |
59 | schema = {
60 | message: {
61 | type: z.string(),
62 | description: "Description of this input parameter",
63 | },
64 | };
65 |
66 | async execute(input: MyToolInput) {
67 | // Your tool logic here
68 | return \`Processed: \${input.message}\`;
69 | }
70 | }
71 |
72 | export default MyTool;
73 | \`\`\`
74 |
75 | ## Publishing to npm
76 |
77 | 1. Update your package.json:
78 | - Ensure \`name\` is unique and follows npm naming conventions
79 | - Set appropriate \`version\`
80 | - Add \`description\`, \`author\`, \`license\`, etc.
81 | - Check \`bin\` points to the correct entry file
82 |
83 | 2. Build and test locally:
84 | \`\`\`bash
85 | npm run build
86 | npm link
87 | ${projectName} # Test your CLI locally
88 | \`\`\`
89 |
90 | 3. Login to npm (create account if necessary):
91 | \`\`\`bash
92 | npm login
93 | \`\`\`
94 |
95 | 4. Publish your package:
96 | \`\`\`bash
97 | npm publish
98 | \`\`\`
99 |
100 | After publishing, users can add it to their claude desktop client (read below) or run it with npx
101 | \`\`\`
102 |
103 | ## Using with Claude Desktop
104 |
105 | ### Local Development
106 |
107 | Add this configuration to your Claude Desktop config file:
108 |
109 | **MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
110 | **Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
111 |
112 | \`\`\`json
113 | {
114 | "mcpServers": {
115 | "${projectName}": {
116 | "command": "node",
117 | "args":["/absolute/path/to/${projectName}/dist/index.js"]
118 | }
119 | }
120 | }
121 | \`\`\`
122 |
123 | ### After Publishing
124 |
125 | Add this configuration to your Claude Desktop config file:
126 |
127 | **MacOS**: \`~/Library/Application Support/Claude/claude_desktop_config.json\`
128 | **Windows**: \`%APPDATA%/Claude/claude_desktop_config.json\`
129 |
130 | \`\`\`json
131 | {
132 | "mcpServers": {
133 | "${projectName}": {
134 | "command": "npx",
135 | "args": ["${projectName}"]
136 | }
137 | }
138 | }
139 | \`\`\`
140 |
141 | ## Building and Testing
142 |
143 | 1. Make changes to your tools
144 | 2. Run \`npm run build\` to compile
145 | 3. The server will automatically load your tools on startup
146 |
147 | ## Learn More
148 |
149 | - [MCP Framework Github](https://github.com/QuantGeekDev/mcp-framework)
150 | - [MCP Framework Docs](https://mcp-framework.com)
151 | `;
152 | }
153 |
--------------------------------------------------------------------------------
/src/cli/utils/string-utils.ts:
--------------------------------------------------------------------------------
1 | export function toPascalCase(str: string): string {
2 | return str
3 | .split(/[-_]/)
4 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
5 | .join("");
6 | }
7 |
--------------------------------------------------------------------------------
/src/cli/utils/validate-project.ts:
--------------------------------------------------------------------------------
1 |
2 | import { readFile } from "fs/promises";
3 | import { findUp } from 'find-up';
4 | import { logger } from "../../core/Logger.js";
5 |
6 |
7 | export async function validateMCPProject() {
8 | try {
9 | const packageJsonPath = await findUp('package.json');
10 |
11 | if (!packageJsonPath) {
12 | throw new Error("Could not find package.json in current directory or any parent directories");
13 | }
14 | const package_json = JSON.parse(await readFile(packageJsonPath, "utf-8"));
15 | if (
16 | !package_json.dependencies?.["mcp-framework"] &&
17 | !package_json.devDependencies?.["mcp-framework"]
18 | ) {
19 | throw new Error(
20 | "This directory is not an MCP project (mcp-framework not found in dependencies or devDependencies)"
21 | );
22 | }
23 | } catch (error) {
24 | console.error("Error: Must be run from an MCP project directory");
25 | logger.error(`Project validation failed: ${(error as Error).message}`);
26 | process.exit(1);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/core/Logger.ts:
--------------------------------------------------------------------------------
1 | import { createWriteStream, WriteStream } from "fs";
2 | import { join } from "path";
3 | import { mkdir } from "fs/promises";
4 |
5 | export class Logger {
6 | private static instance: Logger;
7 | private logStream: WriteStream | null = null;
8 | private logFilePath: string = '';
9 | private logToFile: boolean = false;
10 |
11 | private constructor() {
12 | this.logToFile = process.env.MCP_ENABLE_FILE_LOGGING === 'true';
13 |
14 | if (this.logToFile) {
15 | const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
16 | const logDir = process.env.MCP_LOG_DIRECTORY || "logs";
17 |
18 | this.initFileLogging(logDir, timestamp);
19 | }
20 |
21 | process.on("exit", () => this.close());
22 | process.on("SIGINT", () => this.close());
23 | process.on("SIGTERM", () => this.close());
24 | }
25 |
26 | private async initFileLogging(logDir: string, timestamp: string): Promise {
27 | try {
28 | await mkdir(logDir, { recursive: true });
29 | this.logFilePath = join(logDir, `mcp-server-${timestamp}.log`);
30 | this.logStream = createWriteStream(this.logFilePath, { flags: "a" });
31 | this.info(`File logging enabled, writing to: ${this.logFilePath}`);
32 | } catch (err) {
33 | process.stderr.write(`Failed to initialize file logging: ${err}\n`);
34 | this.logToFile = false;
35 | }
36 | }
37 |
38 | public static getInstance(): Logger {
39 | if (!Logger.instance) {
40 | Logger.instance = new Logger();
41 | }
42 | return Logger.instance;
43 | }
44 |
45 | private getTimestamp(): string {
46 | return new Date().toISOString();
47 | }
48 |
49 | private formatMessage(level: string, message: string): string {
50 | return `[${this.getTimestamp()}] [${level}] ${message}\n`;
51 | }
52 |
53 | public info(message: string): void {
54 | const formattedMessage = this.formatMessage("INFO", message);
55 | if (this.logToFile && this.logStream) {
56 | this.logStream.write(formattedMessage);
57 | }
58 | process.stderr.write(formattedMessage);
59 | }
60 |
61 | public log(message: string): void {
62 | this.info(message);
63 | }
64 |
65 | public error(message: string): void {
66 | const formattedMessage = this.formatMessage("ERROR", message);
67 | if (this.logToFile && this.logStream) {
68 | this.logStream.write(formattedMessage);
69 | }
70 | process.stderr.write(formattedMessage);
71 | }
72 |
73 | public warn(message: string): void {
74 | const formattedMessage = this.formatMessage("WARN", message);
75 | if (this.logToFile && this.logStream) {
76 | this.logStream.write(formattedMessage);
77 | }
78 | process.stderr.write(formattedMessage);
79 | }
80 |
81 | public debug(message: string): void {
82 | const formattedMessage = this.formatMessage("DEBUG", message);
83 | if (this.logToFile && this.logStream) {
84 | this.logStream.write(formattedMessage);
85 | }
86 | if (process.env.MCP_DEBUG_CONSOLE === 'true') {
87 | process.stderr.write(formattedMessage);
88 | }
89 | }
90 |
91 | public close(): void {
92 | if (this.logStream) {
93 | this.logStream.end();
94 | this.logStream = null;
95 | }
96 | }
97 |
98 | public getLogPath(): string {
99 | return this.logFilePath;
100 | }
101 |
102 | public isFileLoggingEnabled(): boolean {
103 | return this.logToFile;
104 | }
105 | }
106 |
107 | export const logger = Logger.getInstance();
108 |
--------------------------------------------------------------------------------
/src/core/MCPServer.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2 | import {
3 | CallToolRequestSchema,
4 | ListToolsRequestSchema,
5 | ListPromptsRequestSchema,
6 | GetPromptRequestSchema,
7 | ListResourcesRequestSchema,
8 | ListResourceTemplatesRequestSchema,
9 | ReadResourceRequestSchema,
10 | SubscribeRequestSchema,
11 | UnsubscribeRequestSchema,
12 | } from '@modelcontextprotocol/sdk/types.js';
13 | import { ToolProtocol } from '../tools/BaseTool.js';
14 | import { PromptProtocol } from '../prompts/BasePrompt.js';
15 | import { ResourceProtocol } from '../resources/BaseResource.js';
16 | import { readFileSync } from 'fs';
17 | import { join, resolve, dirname } from 'path';
18 | import { logger } from './Logger.js';
19 | import { ToolLoader } from '../loaders/toolLoader.js';
20 | import { PromptLoader } from '../loaders/promptLoader.js';
21 | import { ResourceLoader } from '../loaders/resourceLoader.js';
22 | import { BaseTransport } from '../transports/base.js';
23 | import { StdioServerTransport } from '../transports/stdio/server.js';
24 | import { SSEServerTransport } from '../transports/sse/server.js';
25 | import { SSETransportConfig, DEFAULT_SSE_CONFIG } from '../transports/sse/types.js';
26 | import { HttpStreamTransport } from '../transports/http/server.js';
27 | import { HttpStreamTransportConfig, DEFAULT_HTTP_STREAM_CONFIG } from '../transports/http/types.js';
28 | import { DEFAULT_CORS_CONFIG } from '../transports/sse/types.js';
29 | import { AuthConfig } from '../auth/types.js';
30 | import { createRequire } from 'module';
31 |
32 | const require = createRequire(import.meta.url);
33 | export type TransportType = 'stdio' | 'sse' | 'http-stream';
34 |
35 | export interface TransportConfig {
36 | type: TransportType;
37 | options?: SSETransportConfig | HttpStreamTransportConfig;
38 | auth?: AuthConfig;
39 | }
40 |
41 | export interface MCPServerConfig {
42 | name?: string;
43 | version?: string;
44 | basePath?: string;
45 | transport?: TransportConfig;
46 | }
47 |
48 | export type ServerCapabilities = {
49 | tools?: {
50 | listChanged?: true; // Optional: Indicates support for list change notifications
51 | };
52 | prompts?: {
53 | listChanged?: true; // Optional: Indicates support for list change notifications
54 | };
55 | resources?: {
56 | listChanged?: true; // Optional: Indicates support for list change notifications
57 | subscribe?: true; // Optional: Indicates support for resource subscriptions
58 | };
59 | };
60 |
61 | export class MCPServer {
62 | private server!: Server;
63 | private toolsMap: Map = new Map();
64 | private promptsMap: Map = new Map();
65 | private resourcesMap: Map = new Map();
66 | private toolLoader: ToolLoader;
67 | private promptLoader: PromptLoader;
68 | private resourceLoader: ResourceLoader;
69 | private serverName: string;
70 | private serverVersion: string;
71 | private basePath: string;
72 | private transportConfig: TransportConfig;
73 | private capabilities: ServerCapabilities = {};
74 | private isRunning: boolean = false;
75 | private transport?: BaseTransport;
76 | private shutdownPromise?: Promise;
77 | private shutdownResolve?: () => void;
78 |
79 | constructor(config: MCPServerConfig = {}) {
80 | this.basePath = this.resolveBasePath(config.basePath);
81 | this.serverName = config.name ?? this.getDefaultName();
82 | this.serverVersion = config.version ?? this.getDefaultVersion();
83 | this.transportConfig = config.transport ?? { type: 'stdio' };
84 |
85 | if (this.transportConfig.auth && this.transportConfig.options) {
86 | (this.transportConfig.options as any).auth = this.transportConfig.auth;
87 | } else if (this.transportConfig.auth && !this.transportConfig.options) {
88 | this.transportConfig.options = { auth: this.transportConfig.auth } as any;
89 | }
90 |
91 | logger.info(`Initializing MCP Server: ${this.serverName}@${this.serverVersion}`);
92 | logger.debug(`Base path: ${this.basePath}`);
93 | logger.debug(`Transport config: ${JSON.stringify(this.transportConfig)}`);
94 |
95 | this.toolLoader = new ToolLoader(this.basePath);
96 | this.promptLoader = new PromptLoader(this.basePath);
97 | this.resourceLoader = new ResourceLoader(this.basePath);
98 |
99 | this.server = new Server(
100 | { name: this.serverName, version: this.serverVersion },
101 | { capabilities: this.capabilities }
102 | );
103 | logger.debug(`SDK Server instance created.`);
104 | }
105 |
106 | private resolveBasePath(configPath?: string): string {
107 | if (configPath) {
108 | return configPath;
109 | }
110 | if (process.argv[1]) {
111 | return process.argv[1];
112 | }
113 | return process.cwd();
114 | }
115 |
116 | private createTransport(): BaseTransport {
117 | logger.debug(`Creating transport: ${this.transportConfig.type}`);
118 |
119 | let transport: BaseTransport;
120 | const options = this.transportConfig.options || {};
121 | const authConfig = this.transportConfig.auth ?? (options as any).auth;
122 |
123 | switch (this.transportConfig.type) {
124 | case 'sse': {
125 | const sseConfig: SSETransportConfig = {
126 | ...DEFAULT_SSE_CONFIG,
127 | ...(options as SSETransportConfig),
128 | cors: { ...DEFAULT_CORS_CONFIG, ...(options as SSETransportConfig).cors },
129 | auth: authConfig,
130 | };
131 | transport = new SSEServerTransport(sseConfig);
132 | break;
133 | }
134 | case 'http-stream': {
135 | const httpConfig: HttpStreamTransportConfig = {
136 | ...DEFAULT_HTTP_STREAM_CONFIG,
137 | ...(options as HttpStreamTransportConfig),
138 | cors: {
139 | ...DEFAULT_CORS_CONFIG,
140 | ...((options as HttpStreamTransportConfig).cors || {}),
141 | },
142 | auth: authConfig,
143 | };
144 | logger.debug(`Creating HttpStreamTransport. response mode: ${httpConfig.responseMode}`);
145 | transport = new HttpStreamTransport(httpConfig);
146 | break;
147 | }
148 | case 'stdio':
149 | default:
150 | if (this.transportConfig.type !== 'stdio') {
151 | logger.warn(`Unsupported type '${this.transportConfig.type}', defaulting to stdio.`);
152 | }
153 | transport = new StdioServerTransport();
154 | break;
155 | }
156 |
157 | transport.onclose = () => {
158 | logger.info(`Transport (${transport.type}) closed.`);
159 | if (this.isRunning) {
160 | this.stop().catch((error) => {
161 | logger.error(`Shutdown error after transport close: ${error}`);
162 | process.exit(1);
163 | });
164 | }
165 | };
166 |
167 | transport.onerror = (error: Error) => {
168 | logger.error(`Transport (${transport.type}) error: ${error.message}\n${error.stack}`);
169 | };
170 | return transport;
171 | }
172 |
173 | private readPackageJson(): any {
174 | try {
175 | const projectRoot = process.cwd();
176 | const packagePath = join(projectRoot, 'package.json');
177 |
178 | try {
179 | const packageContent = readFileSync(packagePath, 'utf-8');
180 | const packageJson = JSON.parse(packageContent);
181 | logger.debug(`Successfully read package.json from project root: ${packagePath}`);
182 | return packageJson;
183 | } catch (error) {
184 | logger.warn(`Could not read package.json from project root: ${error}`);
185 | return null;
186 | }
187 | } catch (error) {
188 | logger.warn(`Could not read package.json: ${error}`);
189 | return null;
190 | }
191 | }
192 |
193 | private getDefaultName(): string {
194 | const packageJson = this.readPackageJson();
195 | if (packageJson?.name) {
196 | return packageJson.name;
197 | }
198 | logger.error("Couldn't find project name in package json");
199 | return 'unnamed-mcp-server';
200 | }
201 |
202 | private getDefaultVersion(): string {
203 | const packageJson = this.readPackageJson();
204 | if (packageJson?.version) {
205 | return packageJson.version;
206 | }
207 | return '0.0.0';
208 | }
209 |
210 | private setupHandlers() {
211 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
212 | this.server.setRequestHandler(ListToolsRequestSchema, async (request: any) => {
213 | logger.debug(`Received ListTools request: ${JSON.stringify(request)}`);
214 |
215 | const tools = Array.from(this.toolsMap.values()).map((tool) => tool.toolDefinition);
216 |
217 | logger.debug(`Found ${tools.length} tools to return`);
218 | logger.debug(`Tool definitions: ${JSON.stringify(tools)}`);
219 |
220 | const response = {
221 | tools: tools,
222 | nextCursor: undefined,
223 | };
224 |
225 | logger.debug(`Sending ListTools response: ${JSON.stringify(response)}`);
226 | return response;
227 | });
228 |
229 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
230 | this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
231 | logger.debug(`Tool call request received for: ${request.params.name}`);
232 | logger.debug(`Tool call arguments: ${JSON.stringify(request.params.arguments)}`);
233 |
234 | const tool = this.toolsMap.get(request.params.name);
235 | if (!tool) {
236 | const availableTools = Array.from(this.toolsMap.keys());
237 | const errorMsg = `Unknown tool: ${request.params.name}. Available tools: ${availableTools.join(', ')}`;
238 | logger.error(errorMsg);
239 | throw new Error(errorMsg);
240 | }
241 |
242 | try {
243 | logger.debug(`Executing tool: ${tool.name}`);
244 | const toolRequest = {
245 | params: request.params,
246 | method: 'tools/call' as const,
247 | };
248 |
249 | const result = await tool.toolCall(toolRequest);
250 | logger.debug(`Tool execution successful: ${JSON.stringify(result)}`);
251 | return result;
252 | } catch (error) {
253 | const errorMsg = `Tool execution failed: ${error}`;
254 | logger.error(errorMsg);
255 | throw new Error(errorMsg);
256 | }
257 | });
258 |
259 | if (this.capabilities.prompts) {
260 | this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
261 | return {
262 | prompts: Array.from(this.promptsMap.values()).map((prompt) => prompt.promptDefinition),
263 | };
264 | });
265 |
266 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
267 | this.server.setRequestHandler(GetPromptRequestSchema, async (request: any) => {
268 | const prompt = this.promptsMap.get(request.params.name);
269 | if (!prompt) {
270 | throw new Error(
271 | `Unknown prompt: ${request.params.name}. Available prompts: ${Array.from(
272 | this.promptsMap.keys()
273 | ).join(', ')}`
274 | );
275 | }
276 |
277 | return {
278 | messages: await prompt.getMessages(request.params.arguments),
279 | };
280 | });
281 | }
282 |
283 | if (this.capabilities.resources) {
284 | this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
285 | return {
286 | resources: Array.from(this.resourcesMap.values()).map(
287 | (resource) => resource.resourceDefinition
288 | ),
289 | };
290 | });
291 |
292 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
293 | this.server.setRequestHandler(ReadResourceRequestSchema, async (request: any) => {
294 | const resource = this.resourcesMap.get(request.params.uri);
295 | if (!resource) {
296 | throw new Error(
297 | `Unknown resource: ${request.params.uri}. Available resources: ${Array.from(
298 | this.resourcesMap.keys()
299 | ).join(', ')}`
300 | );
301 | }
302 |
303 | return {
304 | contents: await resource.read(),
305 | };
306 | });
307 |
308 | this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
309 | logger.debug(`Received ListResourceTemplates request`);
310 | const response = {
311 | resourceTemplates: [],
312 | nextCursor: undefined,
313 | };
314 | logger.debug(`Sending ListResourceTemplates response: ${JSON.stringify(response)}`);
315 | return response;
316 | });
317 |
318 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
319 | this.server.setRequestHandler(SubscribeRequestSchema, async (request: any) => {
320 | const resource = this.resourcesMap.get(request.params.uri);
321 | if (!resource) {
322 | throw new Error(`Unknown resource: ${request.params.uri}`);
323 | }
324 |
325 | if (!resource.subscribe) {
326 | throw new Error(`Resource ${request.params.uri} does not support subscriptions`);
327 | }
328 |
329 | await resource.subscribe();
330 | return {};
331 | });
332 |
333 | // TODO: Replace 'any' with the specific inferred request type from the SDK schema if available
334 | this.server.setRequestHandler(UnsubscribeRequestSchema, async (request: any) => {
335 | const resource = this.resourcesMap.get(request.params.uri);
336 | if (!resource) {
337 | throw new Error(`Unknown resource: ${request.params.uri}`);
338 | }
339 |
340 | if (!resource.unsubscribe) {
341 | throw new Error(`Resource ${request.params.uri} does not support subscriptions`);
342 | }
343 |
344 | await resource.unsubscribe();
345 | return {};
346 | });
347 | }
348 | }
349 |
350 | private async detectCapabilities(): Promise {
351 | if (await this.toolLoader.hasTools()) {
352 | this.capabilities.tools = {};
353 | logger.debug('Tools capability enabled');
354 | }
355 |
356 | if (await this.promptLoader.hasPrompts()) {
357 | this.capabilities.prompts = {};
358 | logger.debug('Prompts capability enabled');
359 | }
360 |
361 | if (await this.resourceLoader.hasResources()) {
362 | this.capabilities.resources = {};
363 | logger.debug('Resources capability enabled');
364 | }
365 |
366 | (this.server as any).updateCapabilities?.(this.capabilities);
367 | logger.debug(`Capabilities updated: ${JSON.stringify(this.capabilities)}`);
368 |
369 | return this.capabilities;
370 | }
371 |
372 | private getSdkVersion(): string {
373 | try {
374 | const sdkSpecificFile = require.resolve('@modelcontextprotocol/sdk/server/index.js');
375 |
376 | const sdkRootDir = resolve(dirname(sdkSpecificFile), '..', '..', '..');
377 |
378 | const correctPackageJsonPath = join(sdkRootDir, 'package.json');
379 |
380 | const packageContent = readFileSync(correctPackageJsonPath, 'utf-8');
381 |
382 | const packageJson = JSON.parse(packageContent);
383 |
384 | if (packageJson?.version) {
385 | logger.debug(`Found SDK version: ${packageJson.version}`);
386 | return packageJson.version;
387 | } else {
388 | logger.warn('Could not determine SDK version from its package.json.');
389 | return 'unknown';
390 | }
391 | } catch (error: any) {
392 | logger.warn(`Failed to read SDK package.json: ${error.message}`);
393 | return 'unknown';
394 | }
395 | }
396 |
397 | async start() {
398 | try {
399 | if (this.isRunning) {
400 | throw new Error('Server is already running');
401 | }
402 | this.isRunning = true;
403 |
404 | const frameworkPackageJson = require('../../package.json');
405 | const frameworkVersion = frameworkPackageJson.version || 'unknown';
406 | const sdkVersion = this.getSdkVersion();
407 | logger.info(`Starting MCP server: (Framework: ${frameworkVersion}, SDK: ${sdkVersion})...`);
408 |
409 | const tools = await this.toolLoader.loadTools();
410 | this.toolsMap = new Map(tools.map((tool: ToolProtocol) => [tool.name, tool]));
411 |
412 | const prompts = await this.promptLoader.loadPrompts();
413 | this.promptsMap = new Map(prompts.map((prompt: PromptProtocol) => [prompt.name, prompt]));
414 |
415 | const resources = await this.resourceLoader.loadResources();
416 | this.resourcesMap = new Map(
417 | resources.map((resource: ResourceProtocol) => [resource.uri, resource])
418 | );
419 |
420 | await this.detectCapabilities();
421 | logger.info(`Capabilities detected: ${JSON.stringify(this.capabilities)}`);
422 |
423 | this.setupHandlers();
424 |
425 | this.transport = this.createTransport();
426 |
427 | logger.info(`Connecting transport (${this.transport.type}) to SDK Server...`);
428 | await this.server.connect(this.transport);
429 |
430 | logger.info(
431 | `Started ${this.serverName}@${this.serverVersion} successfully on transport ${this.transport.type}`
432 | );
433 |
434 | logger.info(`Tools (${tools.length}): ${tools.map((t) => t.name).join(', ') || 'None'}`);
435 | if (this.capabilities.prompts) {
436 | logger.info(
437 | `Prompts (${prompts.length}): ${prompts.map((p) => p.name).join(', ') || 'None'}`
438 | );
439 | }
440 | if (this.capabilities.resources) {
441 | logger.info(
442 | `Resources (${resources.length}): ${resources.map((r) => r.uri).join(', ') || 'None'}`
443 | );
444 | }
445 |
446 | const shutdownHandler = async (signal: string) => {
447 | if (!this.isRunning) return;
448 | logger.info(`Received ${signal}. Shutting down...`);
449 | try {
450 | await this.stop();
451 | } catch (e: any) {
452 | logger.error(`Shutdown error via ${signal}: ${e.message}`);
453 | process.exit(1);
454 | }
455 | };
456 |
457 | process.on('SIGINT', () => shutdownHandler('SIGINT'));
458 | process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
459 |
460 | this.shutdownPromise = new Promise((resolve) => {
461 | this.shutdownResolve = resolve;
462 | });
463 |
464 | logger.info('Server running and ready.');
465 | await this.shutdownPromise;
466 | } catch (error: any) {
467 | logger.error(`Server failed to start: ${error.message}\n${error.stack}`);
468 | this.isRunning = false;
469 | throw error;
470 | }
471 | }
472 |
473 | async stop() {
474 | if (!this.isRunning) {
475 | logger.debug('Stop called, but server not running.');
476 | return;
477 | }
478 |
479 | try {
480 | logger.info('Stopping server...');
481 |
482 | let transportError: Error | null = null;
483 | let sdkServerError: Error | null = null;
484 |
485 | if (this.transport) {
486 | try {
487 | logger.debug(`Closing transport (${this.transport.type})...`);
488 | await this.transport.close();
489 | logger.info(`Transport closed.`);
490 | } catch (e: any) {
491 | transportError = e;
492 | logger.error(`Error closing transport: ${e.message}`);
493 | }
494 | this.transport = undefined;
495 | }
496 |
497 | if (this.server) {
498 | try {
499 | logger.debug('Closing SDK Server...');
500 | await this.server.close();
501 | logger.info('SDK Server closed.');
502 | } catch (e: any) {
503 | sdkServerError = e;
504 | logger.error(`Error closing SDK Server: ${e.message}`);
505 | }
506 | }
507 |
508 | this.isRunning = false;
509 |
510 | if (this.shutdownResolve) {
511 | this.shutdownResolve();
512 | logger.debug('Shutdown promise resolved.');
513 | } else {
514 | logger.warn('Shutdown resolve function not found.');
515 | }
516 |
517 | if (transportError || sdkServerError) {
518 | logger.error('Errors occurred during server stop.');
519 | throw new Error(
520 | `Server stop failed. TransportError: ${transportError?.message}, SDKServerError: ${sdkServerError?.message}`
521 | );
522 | }
523 |
524 | logger.info('MCP server stopped successfully.');
525 | } catch (error) {
526 | logger.error(`Error stopping server: ${error}`);
527 | throw error;
528 | }
529 | }
530 |
531 | get IsRunning(): boolean {
532 | return this.isRunning;
533 | }
534 | }
535 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./core/MCPServer.js";
2 | export * from "./core/Logger.js";
3 |
4 | export * from "./tools/BaseTool.js";
5 | export * from "./resources/BaseResource.js";
6 | export * from "./prompts/BasePrompt.js";
7 |
8 | export * from "./auth/index.js";
9 |
10 | export type { SSETransportConfig } from "./transports/sse/types.js";
11 | export type { HttpStreamTransportConfig } from "./transports/http/types.js";
12 | export { HttpStreamTransport } from "./transports/http/server.js";
13 |
--------------------------------------------------------------------------------
/src/loaders/promptLoader.ts:
--------------------------------------------------------------------------------
1 | import { PromptProtocol } from "../prompts/BasePrompt.js";
2 | import { join, dirname } from "path";
3 | import { promises as fs } from "fs";
4 | import { logger } from "../core/Logger.js";
5 |
6 | export class PromptLoader {
7 | private readonly PROMPTS_DIR: string;
8 | private readonly EXCLUDED_FILES = ["BasePrompt.js", "*.test.js", "*.spec.js"];
9 |
10 | constructor(basePath?: string) {
11 | const mainModulePath = basePath || process.argv[1];
12 | this.PROMPTS_DIR = join(dirname(mainModulePath), "prompts");
13 | logger.debug(
14 | `Initialized PromptLoader with directory: ${this.PROMPTS_DIR}`
15 | );
16 | }
17 |
18 | async hasPrompts(): Promise {
19 | try {
20 | const stats = await fs.stat(this.PROMPTS_DIR);
21 | if (!stats.isDirectory()) {
22 | logger.debug("Prompts path exists but is not a directory");
23 | return false;
24 | }
25 |
26 | const files = await fs.readdir(this.PROMPTS_DIR);
27 | const hasValidFiles = files.some((file) => this.isPromptFile(file));
28 | logger.debug(`Prompts directory has valid files: ${hasValidFiles}`);
29 | return hasValidFiles;
30 | } catch (error) {
31 | logger.debug(`No prompts directory found: ${(error as Error).message}`);
32 | return false;
33 | }
34 | }
35 |
36 | private isPromptFile(file: string): boolean {
37 | if (!file.endsWith(".js")) return false;
38 | const isExcluded = this.EXCLUDED_FILES.some((pattern) => {
39 | if (pattern.includes("*")) {
40 | const regex = new RegExp(pattern.replace("*", ".*"));
41 | return regex.test(file);
42 | }
43 | return file === pattern;
44 | });
45 |
46 | logger.debug(
47 | `Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
48 | );
49 | return !isExcluded;
50 | }
51 |
52 | private validatePrompt(prompt: any): prompt is PromptProtocol {
53 | const isValid = Boolean(
54 | prompt &&
55 | typeof prompt.name === "string" &&
56 | prompt.promptDefinition &&
57 | typeof prompt.getMessages === "function"
58 | );
59 |
60 | if (isValid) {
61 | logger.debug(`Validated prompt: ${prompt.name}`);
62 | } else {
63 | logger.warn(`Invalid prompt found: missing required properties`);
64 | }
65 |
66 | return isValid;
67 | }
68 |
69 | async loadPrompts(): Promise {
70 | try {
71 | logger.debug(`Attempting to load prompts from: ${this.PROMPTS_DIR}`);
72 |
73 | let stats;
74 | try {
75 | stats = await fs.stat(this.PROMPTS_DIR);
76 | } catch (error) {
77 | logger.debug(`No prompts directory found: ${(error as Error).message}`);
78 | return [];
79 | }
80 |
81 | if (!stats.isDirectory()) {
82 | logger.error(`Path is not a directory: ${this.PROMPTS_DIR}`);
83 | return [];
84 | }
85 |
86 | const files = await fs.readdir(this.PROMPTS_DIR);
87 | logger.debug(`Found files in directory: ${files.join(", ")}`);
88 |
89 | const prompts: PromptProtocol[] = [];
90 |
91 | for (const file of files) {
92 | if (!this.isPromptFile(file)) {
93 | continue;
94 | }
95 |
96 | try {
97 | const fullPath = join(this.PROMPTS_DIR, file);
98 | logger.debug(`Attempting to load prompt from: ${fullPath}`);
99 |
100 | const importPath = `file://${fullPath}`;
101 | const { default: PromptClass } = await import(importPath);
102 |
103 | if (!PromptClass) {
104 | logger.warn(`No default export found in ${file}`);
105 | continue;
106 | }
107 |
108 | const prompt = new PromptClass();
109 | if (this.validatePrompt(prompt)) {
110 | prompts.push(prompt);
111 | }
112 | } catch (error) {
113 | logger.error(`Error loading prompt ${file}: ${(error as Error).message}`);
114 | }
115 | }
116 |
117 | logger.debug(
118 | `Successfully loaded ${prompts.length} prompts: ${prompts
119 | .map((p) => p.name)
120 | .join(", ")}`
121 | );
122 | return prompts;
123 | } catch (error) {
124 | logger.error(`Failed to load prompts: ${(error as Error).message}`);
125 | return [];
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/loaders/resourceLoader.ts:
--------------------------------------------------------------------------------
1 | import { ResourceProtocol } from "../resources/BaseResource.js";
2 | import { join, dirname } from "path";
3 | import { promises as fs } from "fs";
4 | import { logger } from "../core/Logger.js";
5 |
6 | export class ResourceLoader {
7 | private readonly RESOURCES_DIR: string;
8 | private readonly EXCLUDED_FILES = [
9 | "BaseResource.js",
10 | "*.test.js",
11 | "*.spec.js",
12 | ];
13 |
14 | constructor(basePath?: string) {
15 | const mainModulePath = basePath || process.argv[1];
16 | this.RESOURCES_DIR = join(dirname(mainModulePath), "resources");
17 | logger.debug(
18 | `Initialized ResourceLoader with directory: ${this.RESOURCES_DIR}`
19 | );
20 | }
21 |
22 | async hasResources(): Promise {
23 | try {
24 | const stats = await fs.stat(this.RESOURCES_DIR);
25 | if (!stats.isDirectory()) {
26 | logger.debug("Resources path exists but is not a directory");
27 | return false;
28 | }
29 |
30 | const files = await fs.readdir(this.RESOURCES_DIR);
31 | const hasValidFiles = files.some((file) => this.isResourceFile(file));
32 | logger.debug(`Resources directory has valid files: ${hasValidFiles}`);
33 | return hasValidFiles;
34 | } catch (error) {
35 | logger.debug(`No resources directory found: ${(error as Error).message}`);
36 | return false;
37 | }
38 | }
39 |
40 | private isResourceFile(file: string): boolean {
41 | if (!file.endsWith(".js")) return false;
42 | const isExcluded = this.EXCLUDED_FILES.some((pattern) => {
43 | if (pattern.includes("*")) {
44 | const regex = new RegExp(pattern.replace("*", ".*"));
45 | return regex.test(file);
46 | }
47 | return file === pattern;
48 | });
49 |
50 | logger.debug(
51 | `Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
52 | );
53 | return !isExcluded;
54 | }
55 |
56 | private validateResource(resource: any): resource is ResourceProtocol {
57 | const isValid = Boolean(
58 | resource &&
59 | typeof resource.uri === "string" &&
60 | typeof resource.name === "string" &&
61 | resource.resourceDefinition &&
62 | typeof resource.read === "function"
63 | );
64 |
65 | if (isValid) {
66 | logger.debug(`Validated resource: ${resource.name}`);
67 | } else {
68 | logger.warn(`Invalid resource found: missing required properties`);
69 | }
70 |
71 | return isValid;
72 | }
73 |
74 | async loadResources(): Promise {
75 | try {
76 | logger.debug(`Attempting to load resources from: ${this.RESOURCES_DIR}`);
77 |
78 | let stats;
79 | try {
80 | stats = await fs.stat(this.RESOURCES_DIR);
81 | } catch (error) {
82 | logger.debug(`No resources directory found: ${(error as Error).message}`);
83 | return [];
84 | }
85 |
86 | if (!stats.isDirectory()) {
87 | logger.error(`Path is not a directory: ${this.RESOURCES_DIR}`);
88 | return [];
89 | }
90 |
91 | const files = await fs.readdir(this.RESOURCES_DIR);
92 | logger.debug(`Found files in directory: ${files.join(", ")}`);
93 |
94 | const resources: ResourceProtocol[] = [];
95 |
96 | for (const file of files) {
97 | if (!this.isResourceFile(file)) {
98 | continue;
99 | }
100 |
101 | try {
102 | const fullPath = join(this.RESOURCES_DIR, file);
103 | logger.debug(`Attempting to load resource from: ${fullPath}`);
104 |
105 | const importPath = `file://${fullPath}`;
106 | const { default: ResourceClass } = await import(importPath);
107 |
108 | if (!ResourceClass) {
109 | logger.warn(`No default export found in ${file}`);
110 | continue;
111 | }
112 |
113 | const resource = new ResourceClass();
114 | if (this.validateResource(resource)) {
115 | resources.push(resource);
116 | }
117 | } catch (error) {
118 | logger.error(`Error loading resource ${file}: ${(error as Error).message}`);
119 | }
120 | }
121 |
122 | logger.debug(
123 | `Successfully loaded ${resources.length} resources: ${resources
124 | .map((r) => r.name)
125 | .join(", ")}`
126 | );
127 | return resources;
128 | } catch (error) {
129 | logger.error(`Failed to load resources: ${(error as Error).message}`);
130 | return [];
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/loaders/toolLoader.ts:
--------------------------------------------------------------------------------
1 | import { ToolProtocol } from "../tools/BaseTool.js";
2 | import { join, dirname } from "path";
3 | import { promises as fs } from "fs";
4 | import { logger } from "../core/Logger.js";
5 |
6 | export class ToolLoader {
7 | private readonly TOOLS_DIR: string;
8 | private readonly EXCLUDED_FILES = ["BaseTool.js", "*.test.js", "*.spec.js"];
9 |
10 | constructor(basePath?: string) {
11 | const mainModulePath = basePath || process.argv[1];
12 | this.TOOLS_DIR = join(dirname(mainModulePath), "tools");
13 | logger.debug(`Initialized ToolLoader with directory: ${this.TOOLS_DIR}`);
14 | }
15 |
16 | async hasTools(): Promise {
17 | try {
18 | const stats = await fs.stat(this.TOOLS_DIR);
19 | if (!stats.isDirectory()) {
20 | logger.debug("Tools path exists but is not a directory");
21 | return false;
22 | }
23 |
24 | const files = await fs.readdir(this.TOOLS_DIR);
25 | const hasValidFiles = files.some((file) => this.isToolFile(file));
26 | logger.debug(`Tools directory has valid files: ${hasValidFiles}`);
27 | return hasValidFiles;
28 | } catch (error) {
29 | logger.debug(`No tools directory found: ${(error as Error).message}`);
30 | return false;
31 | }
32 | }
33 |
34 | private isToolFile(file: string): boolean {
35 | if (!file.endsWith(".js")) return false;
36 | const isExcluded = this.EXCLUDED_FILES.some((pattern) => {
37 | if (pattern.includes("*")) {
38 | const regex = new RegExp(pattern.replace("*", ".*"));
39 | return regex.test(file);
40 | }
41 | return file === pattern;
42 | });
43 |
44 | logger.debug(
45 | `Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
46 | );
47 | return !isExcluded;
48 | }
49 |
50 | private validateTool(tool: any): tool is ToolProtocol {
51 | const isValid = Boolean(
52 | tool &&
53 | typeof tool.name === "string" &&
54 | tool.toolDefinition &&
55 | typeof tool.toolCall === "function"
56 | );
57 |
58 | if (isValid) {
59 | logger.debug(`Validated tool: ${tool.name}`);
60 | } else {
61 | logger.warn(`Invalid tool found: missing required properties`);
62 | }
63 |
64 | return isValid;
65 | }
66 |
67 | async loadTools(): Promise {
68 | try {
69 | logger.debug(`Attempting to load tools from: ${this.TOOLS_DIR}`);
70 |
71 | let stats;
72 | try {
73 | stats = await fs.stat(this.TOOLS_DIR);
74 | } catch (error) {
75 | logger.debug(`No tools directory found: ${(error as Error).message}`);
76 | return [];
77 | }
78 |
79 | if (!stats.isDirectory()) {
80 | logger.error(`Path is not a directory: ${this.TOOLS_DIR}`);
81 | return [];
82 | }
83 |
84 | const files = await fs.readdir(this.TOOLS_DIR);
85 | logger.debug(`Found files in directory: ${files.join(", ")}`);
86 |
87 | const tools: ToolProtocol[] = [];
88 |
89 | for (const file of files) {
90 | if (!this.isToolFile(file)) {
91 | continue;
92 | }
93 |
94 | try {
95 | const fullPath = join(this.TOOLS_DIR, file);
96 | logger.debug(`Attempting to load tool from: ${fullPath}`);
97 |
98 | const importPath = `file://${fullPath}`;
99 | const { default: ToolClass } = await import(importPath);
100 |
101 | if (!ToolClass) {
102 | logger.warn(`No default export found in ${file}`);
103 | continue;
104 | }
105 |
106 | const tool = new ToolClass();
107 | if (this.validateTool(tool)) {
108 | tools.push(tool);
109 | }
110 | } catch (error) {
111 | logger.error(`Error loading tool ${file}: ${(error as Error).message}`);
112 | }
113 | }
114 |
115 | logger.debug(
116 | `Successfully loaded ${tools.length} tools: ${tools
117 | .map((t) => t.name)
118 | .join(", ")}`
119 | );
120 | return tools;
121 | } catch (error) {
122 | logger.error(`Failed to load tools: ${(error as Error).message}`);
123 | return [];
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/prompts/BasePrompt.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export type PromptArgumentSchema = {
4 | [K in keyof T]: {
5 | type: z.ZodType;
6 | description: string;
7 | required?: boolean;
8 | };
9 | };
10 |
11 | export type PromptArguments> = {
12 | [K in keyof T]: z.infer;
13 | };
14 |
15 | export interface PromptProtocol {
16 | name: string;
17 | description: string;
18 | promptDefinition: {
19 | name: string;
20 | description: string;
21 | arguments?: Array<{
22 | name: string;
23 | description: string;
24 | required?: boolean;
25 | }>;
26 | };
27 | getMessages(args?: Record): Promise<
28 | Array<{
29 | role: string;
30 | content: {
31 | type: string;
32 | text: string;
33 | resource?: {
34 | uri: string;
35 | text: string;
36 | mimeType: string;
37 | };
38 | };
39 | }>
40 | >;
41 | }
42 |
43 | export abstract class MCPPrompt = {}>
44 | implements PromptProtocol
45 | {
46 | abstract name: string;
47 | abstract description: string;
48 | protected abstract schema: PromptArgumentSchema;
49 |
50 | get promptDefinition() {
51 | return {
52 | name: this.name,
53 | description: this.description,
54 | arguments: Object.entries(this.schema).map(([name, schema]) => ({
55 | name,
56 | description: schema.description,
57 | required: schema.required ?? false,
58 | })),
59 | };
60 | }
61 |
62 | protected abstract generateMessages(args: TArgs): Promise<
63 | Array<{
64 | role: string;
65 | content: {
66 | type: string;
67 | text: string;
68 | resource?: {
69 | uri: string;
70 | text: string;
71 | mimeType: string;
72 | };
73 | };
74 | }>
75 | >;
76 |
77 | async getMessages(args: Record = {}) {
78 | const zodSchema = z.object(
79 | Object.fromEntries(
80 | Object.entries(this.schema).map(([key, schema]) => [key, schema.type])
81 | )
82 | );
83 |
84 | const validatedArgs = (await zodSchema.parse(args)) as TArgs;
85 | return this.generateMessages(validatedArgs);
86 | }
87 |
88 | protected async fetch(url: string, init?: RequestInit): Promise {
89 | const response = await fetch(url, init);
90 | if (!response.ok) {
91 | throw new Error(`HTTP error! status: ${response.status}`);
92 | }
93 | return response.json();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/resources/BaseResource.ts:
--------------------------------------------------------------------------------
1 | export type ResourceContent = {
2 | uri: string;
3 | mimeType?: string;
4 | text?: string;
5 | blob?: string;
6 | };
7 |
8 | export type ResourceDefinition = {
9 | uri: string;
10 | name: string;
11 | description?: string;
12 | mimeType?: string;
13 | };
14 |
15 | export type ResourceTemplateDefinition = {
16 | uriTemplate: string;
17 | name: string;
18 | description?: string;
19 | mimeType?: string;
20 | };
21 |
22 | export interface ResourceProtocol {
23 | uri: string;
24 | name: string;
25 | description?: string;
26 | mimeType?: string;
27 | resourceDefinition: ResourceDefinition;
28 | read(): Promise;
29 | subscribe?(): Promise;
30 | unsubscribe?(): Promise;
31 | }
32 |
33 | export abstract class MCPResource implements ResourceProtocol {
34 | abstract uri: string;
35 | abstract name: string;
36 | description?: string;
37 | mimeType?: string;
38 |
39 | get resourceDefinition(): ResourceDefinition {
40 | return {
41 | uri: this.uri,
42 | name: this.name,
43 | description: this.description,
44 | mimeType: this.mimeType,
45 | };
46 | }
47 |
48 | abstract read(): Promise;
49 |
50 | async subscribe?(): Promise {
51 | throw new Error("Subscription not implemented for this resource");
52 | }
53 |
54 | async unsubscribe?(): Promise {
55 | throw new Error("Unsubscription not implemented for this resource");
56 | }
57 |
58 | protected async fetch(url: string, init?: RequestInit): Promise {
59 | const response = await fetch(url, init);
60 | if (!response.ok) {
61 | throw new Error(`HTTP error! status: ${response.status}`);
62 | }
63 | return response.json();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/tools/BaseTool.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { Tool as SDKTool } from "@modelcontextprotocol/sdk/types.js";
3 | import { ImageContent } from "../transports/utils/image-handler.js";
4 |
5 | export type ToolInputSchema = {
6 | [K in keyof T]: {
7 | type: z.ZodType;
8 | description: string;
9 | };
10 | };
11 |
12 | export type ToolInput> = {
13 | [K in keyof T]: z.infer;
14 | };
15 |
16 | export type TextContent = {
17 | type: "text";
18 | text: string;
19 | };
20 |
21 | export type ErrorContent = {
22 | type: "error";
23 | text: string;
24 | };
25 |
26 | export type ToolContent = TextContent | ErrorContent | ImageContent;
27 |
28 | export type ToolResponse = {
29 | content: ToolContent[];
30 | };
31 |
32 | export interface ToolProtocol extends SDKTool {
33 | name: string;
34 | description: string;
35 | toolDefinition: {
36 | name: string;
37 | description: string;
38 | inputSchema: {
39 | type: "object";
40 | properties?: Record;
41 | required?: string[];
42 | };
43 | };
44 | toolCall(request: {
45 | params: { name: string; arguments?: Record };
46 | }): Promise;
47 | }
48 |
49 | export abstract class MCPTool = {}>
50 | implements ToolProtocol
51 | {
52 | abstract name: string;
53 | abstract description: string;
54 | protected abstract schema: ToolInputSchema;
55 | protected useStringify: boolean = true;
56 | [key: string]: unknown;
57 |
58 | get inputSchema(): { type: "object"; properties?: Record; required?: string[] } {
59 | const properties: Record = {};
60 | const required: string[] = [];
61 |
62 | Object.entries(this.schema).forEach(([key, schema]) => {
63 | // Determine the correct JSON schema type (unwrapping optional if necessary)
64 | const jsonType = this.getJsonSchemaType(schema.type);
65 | properties[key] = {
66 | type: jsonType,
67 | description: schema.description,
68 | };
69 |
70 | // If the field is not an optional, add it to the required array.
71 | if (!(schema.type instanceof z.ZodOptional)) {
72 | required.push(key);
73 | }
74 | });
75 |
76 | const inputSchema: { type: "object"; properties: Record; required?: string[] } = {
77 | type: "object",
78 | properties,
79 | };
80 |
81 | if (required.length > 0) {
82 | inputSchema.required = required;
83 | }
84 |
85 | return inputSchema;
86 | }
87 |
88 | get toolDefinition() {
89 | return {
90 | name: this.name,
91 | description: this.description,
92 | inputSchema: this.inputSchema,
93 | };
94 | }
95 |
96 | protected abstract execute(input: TInput): Promise;
97 |
98 | async toolCall(request: {
99 | params: { name: string; arguments?: Record };
100 | }): Promise {
101 | try {
102 | const args = request.params.arguments || {};
103 | const validatedInput = await this.validateInput(args);
104 | const result = await this.execute(validatedInput);
105 | return this.createSuccessResponse(result);
106 | } catch (error) {
107 | return this.createErrorResponse(error as Error);
108 | }
109 | }
110 |
111 | private async validateInput(args: Record): Promise {
112 | const zodSchema = z.object(
113 | Object.fromEntries(
114 | Object.entries(this.schema).map(([key, schema]) => [key, schema.type])
115 | )
116 | );
117 |
118 | return zodSchema.parse(args) as TInput;
119 | }
120 |
121 | private getJsonSchemaType(zodType: z.ZodType): string {
122 | // Unwrap optional types to correctly determine the JSON schema type.
123 | let currentType = zodType;
124 | if (currentType instanceof z.ZodOptional) {
125 | currentType = currentType.unwrap();
126 | }
127 |
128 | if (currentType instanceof z.ZodString) return "string";
129 | if (currentType instanceof z.ZodNumber) return "number";
130 | if (currentType instanceof z.ZodBoolean) return "boolean";
131 | if (currentType instanceof z.ZodArray) return "array";
132 | if (currentType instanceof z.ZodObject) return "object";
133 | return "string";
134 | }
135 |
136 | protected createSuccessResponse(data: unknown): ToolResponse {
137 | if (this.isImageContent(data)) {
138 | return {
139 | content: [data],
140 | };
141 | }
142 |
143 | if (Array.isArray(data)) {
144 | const validContent = data.filter(item => this.isValidContent(item)) as ToolContent[];
145 | if (validContent.length > 0) {
146 | return {
147 | content: validContent,
148 | };
149 | }
150 | }
151 |
152 | return {
153 | content: [{
154 | type: "text",
155 | text: this.useStringify ? JSON.stringify(data) : String(data)
156 | }],
157 | };
158 | }
159 |
160 | protected createErrorResponse(error: Error): ToolResponse {
161 | return {
162 | content: [{ type: "error", text: error.message }],
163 | };
164 | }
165 |
166 | private isImageContent(data: unknown): data is ImageContent {
167 | return (
168 | typeof data === "object" &&
169 | data !== null &&
170 | "type" in data &&
171 | data.type === "image" &&
172 | "data" in data &&
173 | "mimeType" in data &&
174 | typeof (data as ImageContent).data === "string" &&
175 | typeof (data as ImageContent).mimeType === "string"
176 | );
177 | }
178 |
179 | private isTextContent(data: unknown): data is TextContent {
180 | return (
181 | typeof data === "object" &&
182 | data !== null &&
183 | "type" in data &&
184 | data.type === "text" &&
185 | "text" in data &&
186 | typeof (data as TextContent).text === "string"
187 | );
188 | }
189 |
190 | private isErrorContent(data: unknown): data is ErrorContent {
191 | return (
192 | typeof data === "object" &&
193 | data !== null &&
194 | "type" in data &&
195 | data.type === "error" &&
196 | "text" in data &&
197 | typeof (data as ErrorContent).text === "string"
198 | );
199 | }
200 |
201 | private isValidContent(data: unknown): data is ToolContent {
202 | return (
203 | this.isImageContent(data) ||
204 | this.isTextContent(data) ||
205 | this.isErrorContent(data)
206 | );
207 | }
208 |
209 | protected async fetch(url: string, init?: RequestInit): Promise {
210 | const response = await fetch(url, init);
211 | if (!response.ok) {
212 | throw new Error(`HTTP error! status: ${response.status}`);
213 | }
214 | return response.json();
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/transports/base.ts:
--------------------------------------------------------------------------------
1 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2 | import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
3 |
4 | /**
5 | * Base transport interface
6 | */
7 | export interface BaseTransport extends Transport {
8 | // Properties from SDK Transport (explicitly listed for clarity/safety)
9 | onclose?: (() => void) | undefined;
10 | onerror?: ((error: Error) => void) | undefined;
11 | onmessage?: ((message: JSONRPCMessage) => void) | undefined;
12 |
13 | // Methods from SDK Transport (explicitly listed for clarity/safety)
14 | send(message: JSONRPCMessage): Promise;
15 | close(): Promise;
16 | start(): Promise; // Assuming start is also needed, mirroring AbstractTransport
17 |
18 | /**
19 | * The type of transport (e.g., "stdio", "sse")
20 | */
21 | readonly type: string;
22 |
23 | /**
24 | * Returns whether the transport is currently running
25 | */
26 | isRunning(): boolean;
27 | }
28 |
29 | /**
30 | * Abstract base class for transports that implements common functionality
31 | */
32 | export abstract class AbstractTransport implements BaseTransport {
33 | abstract readonly type: string;
34 |
35 | protected _onclose?: () => void;
36 | protected _onerror?: (error: Error) => void;
37 | protected _onmessage?: (message: JSONRPCMessage) => void;
38 |
39 | set onclose(handler: (() => void) | undefined) {
40 | this._onclose = handler;
41 | }
42 |
43 | set onerror(handler: ((error: Error) => void) | undefined) {
44 | this._onerror = handler;
45 | }
46 |
47 | set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) {
48 | this._onmessage = handler;
49 | }
50 |
51 | abstract start(): Promise;
52 | abstract send(message: JSONRPCMessage): Promise;
53 | abstract close(): Promise;
54 | abstract isRunning(): boolean;
55 | }
56 |
--------------------------------------------------------------------------------
/src/transports/http/server.ts:
--------------------------------------------------------------------------------
1 | import { randomUUID } from 'node:crypto';
2 | import { IncomingMessage, ServerResponse, createServer, Server as HttpServer } from 'node:http';
3 | import { AbstractTransport } from '../base.js';
4 | import { JSONRPCMessage, isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
5 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6 | import { HttpStreamTransportConfig } from './types.js';
7 | import { logger } from '../../core/Logger.js';
8 |
9 | export class HttpStreamTransport extends AbstractTransport {
10 | readonly type = 'http-stream';
11 | private _sdkTransport: StreamableHTTPServerTransport;
12 | private _isRunning = false;
13 | private _port: number;
14 | private _server?: HttpServer;
15 | private _endpoint: string;
16 | private _enableJsonResponse: boolean = false;
17 | private _sessionInitialized: boolean = false;
18 |
19 | private _pingInterval?: NodeJS.Timeout;
20 | private _pingTimeouts: Map = new Map();
21 | private _pingFrequency: number;
22 | private _pingTimeout: number;
23 |
24 | constructor(config: HttpStreamTransportConfig = {}) {
25 | super();
26 |
27 | this._port = config.port || 8080;
28 | this._endpoint = config.endpoint || '/mcp';
29 | this._enableJsonResponse = config.responseMode === 'batch';
30 |
31 | this._pingFrequency = config.ping?.frequency ?? 30000; // Default 30 seconds
32 | this._pingTimeout = config.ping?.timeout ?? 10000; // Default 10 seconds
33 |
34 | this._sdkTransport = this.createSdkTransport();
35 |
36 | logger.debug(
37 | `HttpStreamTransport configured with: ${JSON.stringify({
38 | port: this._port,
39 | endpoint: this._endpoint,
40 | responseMode: config.responseMode,
41 | batchTimeout: config.batchTimeout,
42 | maxMessageSize: config.maxMessageSize,
43 | auth: config.auth ? true : false,
44 | cors: config.cors ? true : false,
45 | ping: {
46 | frequency: this._pingFrequency,
47 | timeout: this._pingTimeout,
48 | },
49 | })}`
50 | );
51 | }
52 |
53 | private createSdkTransport(): StreamableHTTPServerTransport {
54 | const transport = new StreamableHTTPServerTransport({
55 | sessionIdGenerator: () => randomUUID(),
56 | onsessioninitialized: (sessionId: string) => {
57 | logger.info(`Session initialized: ${sessionId}`);
58 | this._sessionInitialized = true;
59 | },
60 | enableJsonResponse: this._enableJsonResponse,
61 | });
62 |
63 | transport.onmessage = (message: JSONRPCMessage) => {
64 | if (this.handlePingMessage(message)) {
65 | return;
66 | }
67 |
68 | this._onmessage?.(message);
69 | };
70 |
71 | return transport;
72 | }
73 |
74 | async start(): Promise {
75 | if (this._isRunning) {
76 | throw new Error('HttpStreamTransport already started');
77 | }
78 |
79 | return new Promise((resolve, reject) => {
80 | this._server = createServer(async (req, res) => {
81 | try {
82 | const url = new URL(req.url!, `http://${req.headers.host}`);
83 |
84 | if (url.pathname === this._endpoint) {
85 | // Special handling for POST requests to detect initialization requests
86 | if (req.method === 'POST') {
87 | const contentType = req.headers['content-type'];
88 | if (contentType?.includes('application/json')) {
89 | // Need to intercept body data to check for initialization
90 | let bodyData = '';
91 | req.on('data', (chunk) => {
92 | bodyData += chunk.toString();
93 | });
94 |
95 | req.on('end', async () => {
96 | try {
97 | const jsonData = JSON.parse(bodyData);
98 | const messages = Array.isArray(jsonData) ? jsonData : [jsonData];
99 |
100 | // Check if this is an initialization request AND we already have a session
101 | // Only recreate for subsequent initializations, not the first one
102 | if (messages.some(isInitializeRequest) && this._sessionInitialized) {
103 | logger.info(
104 | 'Received initialization request for existing session, recreating transport'
105 | );
106 |
107 | // Reset session state first
108 | this._sessionInitialized = false;
109 |
110 | // Close the old transport
111 | try {
112 | await this._sdkTransport.close();
113 | } catch (err) {
114 | logger.warn(`Error closing previous transport: ${err}`);
115 | }
116 |
117 | // Create a fresh transport for this new connection
118 | this._sdkTransport = this.createSdkTransport();
119 | await this._sdkTransport.start();
120 | }
121 |
122 | // Forward the original request to the SDK transport
123 | await this._sdkTransport.handleRequest(req, res, jsonData);
124 | } catch (error) {
125 | logger.error(`Error handling JSON data: ${error}`);
126 | if (!res.headersSent) {
127 | res.writeHead(400).end(
128 | JSON.stringify({
129 | jsonrpc: '2.0',
130 | error: {
131 | code: -32700,
132 | message: 'Parse error',
133 | data: String(error),
134 | },
135 | id: null,
136 | })
137 | );
138 | }
139 | }
140 | });
141 | } else {
142 | await this._sdkTransport.handleRequest(req, res);
143 | }
144 | } else if (req.method === 'DELETE') {
145 | // For DELETE requests, reset the session state
146 | this._sessionInitialized = false;
147 | await this._sdkTransport.handleRequest(req, res);
148 | } else {
149 | // For GET requests, just forward to the SDK transport
150 | await this._sdkTransport.handleRequest(req, res);
151 | }
152 | } else {
153 | res.writeHead(404).end('Not Found');
154 | }
155 | } catch (error) {
156 | logger.error(`Error handling request: ${error}`);
157 | if (!res.headersSent) {
158 | res.writeHead(500).end('Internal Server Error');
159 | }
160 | }
161 | });
162 |
163 | this._server.on('error', (error) => {
164 | logger.error(`HTTP server error: ${error}`);
165 | this._onerror?.(error);
166 | if (!this._isRunning) {
167 | reject(error);
168 | }
169 | });
170 |
171 | this._server.on('close', () => {
172 | logger.info('HTTP server closed');
173 | this._isRunning = false;
174 | this._onclose?.();
175 | });
176 |
177 | this._server.listen(this._port, () => {
178 | logger.info(`HTTP server listening on port ${this._port}, endpoint ${this._endpoint}`);
179 |
180 | this._sdkTransport
181 | .start()
182 | .then(() => {
183 | this._isRunning = true;
184 | logger.info(`HttpStreamTransport started successfully on port ${this._port}`);
185 |
186 | this.startPingInterval();
187 |
188 | resolve();
189 | })
190 | .catch((error) => {
191 | logger.error(`Failed to start SDK transport: ${error}`);
192 | this._server?.close();
193 | reject(error);
194 | });
195 | });
196 | });
197 | }
198 |
199 | private startPingInterval(): void {
200 | if (this._pingFrequency > 0) {
201 | logger.debug(
202 | `Starting ping interval with frequency ${this._pingFrequency}ms and timeout ${this._pingTimeout}ms`
203 | );
204 | this._pingInterval = setInterval(() => this.sendPing(), this._pingFrequency);
205 | }
206 | }
207 |
208 | private async sendPing(): Promise {
209 | if (!this._isRunning) {
210 | return;
211 | }
212 |
213 | try {
214 | const pingId = `ping-${Date.now()}`;
215 | const pingRequest: JSONRPCMessage = {
216 | jsonrpc: '2.0' as const,
217 | id: pingId,
218 | method: 'ping',
219 | };
220 |
221 | logger.debug(`Sending ping request: ${JSON.stringify(pingRequest)}`);
222 |
223 | const timeoutId = setTimeout(() => {
224 | logger.warn(
225 | `Ping ${pingId} timed out after ${this._pingTimeout}ms - connection may be stale`
226 | );
227 | this._pingTimeouts.delete(pingId);
228 |
229 | this._onerror?.(new Error(`Ping timeout (${pingId}) - connection may be stale`));
230 | }, this._pingTimeout);
231 |
232 | this._pingTimeouts.set(pingId, timeoutId);
233 |
234 | await this.send(pingRequest);
235 | } catch (error) {
236 | logger.error(`Error sending ping: ${error}`);
237 | }
238 | }
239 |
240 | private handlePingMessage(message: JSONRPCMessage): boolean {
241 | if ('method' in message && message.method === 'ping') {
242 | const id = 'id' in message ? message.id : undefined;
243 | logger.debug(`Received ping request: ${JSON.stringify(message)}`);
244 |
245 | if (id !== undefined) {
246 | const response = {
247 | jsonrpc: '2.0' as const,
248 | id: id,
249 | result: {},
250 | };
251 | logger.debug(`Sending ping response: ${JSON.stringify(response)}`);
252 |
253 | this.send(response).catch((error) => logger.error(`Error responding to ping: ${error}`));
254 | }
255 |
256 | return true;
257 | }
258 |
259 | if (
260 | 'id' in message &&
261 | message.id &&
262 | typeof message.id === 'string' &&
263 | message.id.startsWith('ping-') &&
264 | 'result' in message
265 | ) {
266 | logger.debug(`Received ping response: ${JSON.stringify(message)}`);
267 |
268 | const timeoutId = this._pingTimeouts.get(message.id);
269 | if (timeoutId) {
270 | clearTimeout(timeoutId);
271 | this._pingTimeouts.delete(message.id);
272 | logger.debug(`Cleared timeout for ping response: ${message.id}`);
273 | }
274 |
275 | return true;
276 | }
277 |
278 | return false;
279 | }
280 |
281 | async handleRequest(req: IncomingMessage, res: ServerResponse, body?: any): Promise {
282 | return this._sdkTransport.handleRequest(req, res, body);
283 | }
284 |
285 | async send(message: JSONRPCMessage): Promise {
286 | await this._sdkTransport.send(message);
287 | }
288 |
289 | async close(): Promise {
290 | if (!this._isRunning) {
291 | return;
292 | }
293 |
294 | this._isRunning = false;
295 | this._sessionInitialized = false;
296 |
297 | if (this._pingInterval) {
298 | clearInterval(this._pingInterval);
299 | this._pingInterval = undefined;
300 | }
301 |
302 | for (const timeoutId of this._pingTimeouts.values()) {
303 | clearTimeout(timeoutId);
304 | }
305 | this._pingTimeouts.clear();
306 |
307 | await this._sdkTransport.close();
308 |
309 | return new Promise((resolve) => {
310 | if (!this._server) {
311 | resolve();
312 | return;
313 | }
314 |
315 | this._server.close(() => {
316 | logger.info('HTTP server stopped');
317 | this._server = undefined;
318 | resolve();
319 | });
320 | });
321 | }
322 |
323 | isRunning(): boolean {
324 | return this._isRunning && !!this._server;
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/src/transports/http/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | JSONRPCRequest,
3 | JSONRPCResponse,
4 | JSONRPCMessage,
5 | RequestId,
6 | } from '@modelcontextprotocol/sdk/types.js';
7 |
8 | export { JSONRPCRequest, JSONRPCResponse, JSONRPCMessage, RequestId };
9 |
10 | /**
11 | * Response mode enum
12 | */
13 | export type ResponseMode = 'stream' | 'batch';
14 |
15 | /**
16 | * Configuration interface for the HTTP Stream transport
17 | */
18 | export interface HttpStreamTransportConfig {
19 | /**
20 | * Port to run the HTTP server on, defaults to 8080
21 | */
22 | port?: number;
23 |
24 | /**
25 | * Endpoint path for MCP communication, defaults to "/mcp"
26 | */
27 | endpoint?: string;
28 |
29 | /**
30 | * Configure ping mechanism for connection health verification
31 | */
32 | ping?: {
33 | /**
34 | * Interval in milliseconds for sending ping requests
35 | * Set to 0 to disable pings
36 | * Default: 30000 (30 seconds)
37 | */
38 | frequency?: number;
39 |
40 | /**
41 | * Timeout in milliseconds for waiting for a ping response
42 | * Default: 10000 (10 seconds)
43 | */
44 | timeout?: number;
45 | };
46 |
47 | /**
48 | * Response mode: stream (Server-Sent Events) or batch (JSON)
49 | * Defaults to 'stream'
50 | */
51 | responseMode?: ResponseMode;
52 |
53 | /**
54 | * Timeout in milliseconds for batched messages
55 | * Only applies when responseMode is 'batch'
56 | */
57 | batchTimeout?: number;
58 |
59 | /**
60 | * Maximum message size in bytes
61 | */
62 | maxMessageSize?: number;
63 |
64 | /**
65 | * Authentication configuration
66 | */
67 | auth?: any;
68 |
69 | /**
70 | * CORS configuration
71 | */
72 | cors?: any;
73 | }
74 |
75 | export const DEFAULT_HTTP_STREAM_CONFIG: HttpStreamTransportConfig = {
76 | port: 8080,
77 | endpoint: '/mcp',
78 | responseMode: 'stream',
79 | batchTimeout: 30000,
80 | maxMessageSize: 4 * 1024 * 1024, // 4mb
81 | ping: {
82 | frequency: 30000, // 30 seconds
83 | timeout: 10000, // 10 seconds
84 | },
85 | };
86 |
--------------------------------------------------------------------------------
/src/transports/sse/server.ts:
--------------------------------------------------------------------------------
1 | import { randomUUID } from "node:crypto";
2 | import { IncomingMessage, Server as HttpServer, ServerResponse, createServer } from "node:http";
3 | import { JSONRPCMessage, ClientRequest } from "@modelcontextprotocol/sdk/types.js";
4 | import contentType from "content-type";
5 | import getRawBody from "raw-body";
6 | import { APIKeyAuthProvider } from "../../auth/providers/apikey.js";
7 | import { DEFAULT_AUTH_ERROR } from "../../auth/types.js";
8 | import { AbstractTransport } from "../base.js";
9 | import { DEFAULT_SSE_CONFIG, SSETransportConfig, SSETransportConfigInternal, DEFAULT_CORS_CONFIG, CORSConfig } from "./types.js";
10 | import { logger } from "../../core/Logger.js";
11 | import { getRequestHeader, setResponseHeaders } from "../../utils/headers.js";
12 | import { PING_SSE_MESSAGE } from "../utils/ping-message.js";
13 |
14 |
15 | const SSE_HEADERS = {
16 | "Content-Type": "text/event-stream",
17 | "Cache-Control": "no-cache",
18 | "Connection": "keep-alive"
19 | }
20 |
21 | export class SSEServerTransport extends AbstractTransport {
22 | readonly type = "sse"
23 |
24 | private _server?: HttpServer
25 | private _connections: Map // Map
26 | private _sessionId: string // Server instance ID
27 | private _config: SSETransportConfigInternal
28 |
29 | constructor(config: SSETransportConfig = {}) {
30 | super()
31 | this._connections = new Map()
32 | this._sessionId = randomUUID() // Used to validate POST messages belong to this server instance
33 | this._config = {
34 | ...DEFAULT_SSE_CONFIG,
35 | ...config
36 | }
37 | logger.debug(`SSE transport configured with: ${JSON.stringify({
38 | ...this._config,
39 | auth: this._config.auth ? {
40 | provider: this._config.auth.provider.constructor.name,
41 | endpoints: this._config.auth.endpoints
42 | } : undefined
43 | })}`)
44 | }
45 |
46 | private getCorsHeaders(includeMaxAge: boolean = false): Record {
47 | const corsConfig = {
48 | allowOrigin: DEFAULT_CORS_CONFIG.allowOrigin,
49 | allowMethods: DEFAULT_CORS_CONFIG.allowMethods,
50 | allowHeaders: DEFAULT_CORS_CONFIG.allowHeaders,
51 | exposeHeaders: DEFAULT_CORS_CONFIG.exposeHeaders,
52 | maxAge: DEFAULT_CORS_CONFIG.maxAge,
53 | ...this._config.cors
54 | } as Required
55 |
56 | const headers: Record = {
57 | "Access-Control-Allow-Origin": corsConfig.allowOrigin,
58 | "Access-Control-Allow-Methods": corsConfig.allowMethods,
59 | "Access-Control-Allow-Headers": corsConfig.allowHeaders,
60 | "Access-Control-Expose-Headers": corsConfig.exposeHeaders
61 | }
62 |
63 | if (includeMaxAge) {
64 | headers["Access-Control-Max-Age"] = corsConfig.maxAge
65 | }
66 |
67 | return headers
68 | }
69 |
70 | async start(): Promise {
71 | if (this._server) {
72 | throw new Error("SSE transport already started")
73 | }
74 |
75 | return new Promise((resolve) => {
76 | this._server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
77 | try {
78 | await this.handleRequest(req, res)
79 | } catch (error: any) {
80 | logger.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`)
81 | res.writeHead(500).end("Internal Server Error")
82 | }
83 | })
84 |
85 | this._server.listen(this._config.port, () => {
86 | logger.info(`SSE transport listening on port ${this._config.port}`)
87 | resolve()
88 | })
89 |
90 | this._server.on("error", (error: Error) => {
91 | logger.error(`SSE server error: ${error.message}`)
92 | this._onerror?.(error)
93 | })
94 |
95 | this._server.on("close", () => {
96 | logger.info("SSE server closed")
97 | this._onclose?.()
98 | })
99 | })
100 | }
101 |
102 | private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise {
103 | logger.debug(`Incoming request: ${req.method} ${req.url}`)
104 |
105 | if (req.method === "OPTIONS") {
106 | setResponseHeaders(res, this.getCorsHeaders(true))
107 | res.writeHead(204).end()
108 | return
109 | }
110 |
111 | setResponseHeaders(res, this.getCorsHeaders())
112 |
113 | const url = new URL(req.url!, `http://${req.headers.host}`)
114 | const sessionId = url.searchParams.get("sessionId")
115 |
116 | if (req.method === "GET" && url.pathname === this._config.endpoint) {
117 | if (this._config.auth?.endpoints?.sse) {
118 | const isAuthenticated = await this.handleAuthentication(req, res, "SSE connection")
119 | if (!isAuthenticated) return
120 | }
121 |
122 | // Check if a sessionId was provided in the request
123 | if (sessionId) {
124 | // If sessionId exists but is not in our connections map, it's invalid or inactive
125 | if (!this._connections.has(sessionId)) {
126 | logger.info(`Invalid or inactive session ID in GET request: ${sessionId}. Creating new connection.`);
127 | // Continue execution to create a new connection below
128 | } else {
129 | // If the connection exists and is still active, we could either:
130 | // 1. Return an error (409 Conflict) as a client shouldn't create duplicate connections
131 | // 2. Close the old connection and create a new one
132 | // 3. Keep the old connection and return its details
133 |
134 | // Option 2: Close old connection and create new one
135 | logger.info(`Replacing existing connection for session ID: ${sessionId}`);
136 | this.cleanupConnection(sessionId);
137 | // Continue execution to create a new connection below
138 | }
139 | }
140 |
141 | // Generate a unique ID for this specific connection
142 | const connectionId = randomUUID();
143 | this.setupSSEConnection(res, connectionId);
144 | return;
145 | }
146 |
147 | if (req.method === "POST" && url.pathname === this._config.messageEndpoint) {
148 | // **Connection Validation (User Requested):**
149 | // Check if the 'sessionId' from the POST request URL query parameter
150 | // (which should contain a connectionId provided by the server via the 'endpoint' event)
151 | // corresponds to an active connection in the `_connections` map.
152 | if (!sessionId || !this._connections.has(sessionId)) {
153 | logger.warn(`Invalid or inactive connection ID in POST request URL: ${sessionId}`);
154 | // Use 403 Forbidden as the client is attempting an operation for an invalid/unknown connection
155 | res.writeHead(403).end("Invalid or inactive connection ID");
156 | return;
157 | }
158 |
159 | if (this._config.auth?.endpoints?.messages !== false) {
160 | const isAuthenticated = await this.handleAuthentication(req, res, "message")
161 | if (!isAuthenticated) return
162 | }
163 |
164 | await this.handlePostMessage(req, res)
165 | return
166 | }
167 |
168 | res.writeHead(404).end("Not Found")
169 | }
170 |
171 | private async handleAuthentication(req: IncomingMessage, res: ServerResponse, context: string): Promise {
172 | if (!this._config.auth?.provider) {
173 | return true
174 | }
175 |
176 | const isApiKey = this._config.auth.provider instanceof APIKeyAuthProvider
177 | if (isApiKey) {
178 | const provider = this._config.auth.provider as APIKeyAuthProvider
179 | const headerValue = getRequestHeader(req.headers, provider.getHeaderName())
180 |
181 | if (!headerValue) {
182 | const error = provider.getAuthError?.() || DEFAULT_AUTH_ERROR
183 | res.setHeader("WWW-Authenticate", `ApiKey realm="MCP Server", header="${provider.getHeaderName()}"`)
184 | res.writeHead(error.status).end(JSON.stringify({
185 | error: error.message,
186 | status: error.status,
187 | type: "authentication_error"
188 | }))
189 | return false
190 | }
191 | }
192 |
193 | const authResult = await this._config.auth.provider.authenticate(req)
194 | if (!authResult) {
195 | const error = this._config.auth.provider.getAuthError?.() || DEFAULT_AUTH_ERROR
196 | logger.warn(`Authentication failed for ${context}:`)
197 | logger.warn(`- Client IP: ${req.socket.remoteAddress}`)
198 | logger.warn(`- Error: ${error.message}`)
199 |
200 | if (isApiKey) {
201 | const provider = this._config.auth.provider as APIKeyAuthProvider
202 | res.setHeader("WWW-Authenticate", `ApiKey realm="MCP Server", header="${provider.getHeaderName()}"`)
203 | }
204 |
205 | res.writeHead(error.status).end(JSON.stringify({
206 | error: error.message,
207 | status: error.status,
208 | type: "authentication_error"
209 | }))
210 | return false
211 | }
212 |
213 | logger.info(`Authentication successful for ${context}:`)
214 | logger.info(`- Client IP: ${req.socket.remoteAddress}`)
215 | logger.info(`- Auth Type: ${this._config.auth.provider.constructor.name}`)
216 | return true
217 | }
218 |
219 | private setupSSEConnection(res: ServerResponse, connectionId: string): void {
220 | logger.debug(`Setting up SSE connection: ${connectionId} for server session: ${this._sessionId}`);
221 | const headers = {
222 | ...SSE_HEADERS,
223 | ...this.getCorsHeaders(),
224 | ...this._config.headers
225 | }
226 | setResponseHeaders(res, headers)
227 | logger.debug(`SSE headers set: ${JSON.stringify(headers)}`)
228 |
229 | if (res.socket) {
230 | res.socket.setNoDelay(true)
231 | res.socket.setTimeout(0)
232 | res.socket.setKeepAlive(true, 1000)
233 | logger.debug('Socket optimized for SSE connection');
234 | }
235 | // **Important Change:** The endpoint URL now includes the specific connectionId
236 | // in the 'sessionId' query parameter, as requested by user feedback.
237 | // The client should use this exact URL for subsequent POST messages.
238 | const endpointUrl = `${this._config.messageEndpoint}?sessionId=${connectionId}`;
239 | logger.debug(`Sending endpoint URL for connection ${connectionId}: ${endpointUrl}`);
240 | res.write(`event: endpoint\ndata: ${endpointUrl}\n\n`);
241 | // Send the unique connection ID separately as well for potential client-side use
242 | res.write(`event: connectionId\ndata: ${connectionId}\n\n`);
243 | logger.debug(`Sending initial keep-alive for connection: ${connectionId}`);
244 | const intervalId = setInterval(() => {
245 | const connection = this._connections.get(connectionId);
246 | if (connection && !connection.res.writableEnded) {
247 | try {
248 | connection.res.write(PING_SSE_MESSAGE);
249 | }
250 | catch (error: any) {
251 | logger.error(`Error sending keep-alive for connection ${connectionId}: ${error instanceof Error ? error.message : String(error)}`);
252 | this.cleanupConnection(connectionId);
253 | }
254 | }
255 | else {
256 | // Should not happen if cleanup is working, but clear interval just in case
257 | logger.warn(`Keep-alive interval running for missing/ended connection: ${connectionId}`);
258 | this.cleanupConnection(connectionId); // Will clear interval
259 | }
260 | }, 15000);
261 | this._connections.set(connectionId, { res, intervalId });
262 | const cleanup = () => this.cleanupConnection(connectionId);
263 | res.on("close", () => {
264 | logger.info(`SSE connection closed: ${connectionId}`);
265 | cleanup();
266 | });
267 | res.on("error", (error: Error) => {
268 | logger.error(`SSE connection error for ${connectionId}: ${error.message}`);
269 | this._onerror?.(error);
270 | cleanup();
271 | });
272 | res.on("end", () => {
273 | logger.info(`SSE connection ended: ${connectionId}`);
274 | cleanup();
275 | });
276 | logger.info(`SSE connection established successfully: ${connectionId}`);
277 | }
278 |
279 | private async handlePostMessage(req: IncomingMessage, res: ServerResponse): Promise {
280 | // Check if *any* connection is active, not just the old single _sseResponse
281 | if (this._connections.size === 0) {
282 | logger.warn(`Rejecting message: no active SSE connections for server session ${this._sessionId}`);
283 | // Use 409 Conflict as it indicates the server state prevents fulfilling the request
284 | res.writeHead(409).end("No active SSE connection established");
285 | return;
286 | }
287 |
288 | let currentMessage: { id?: string | number; method?: string } = {}
289 |
290 | try {
291 | const rawMessage = (req as any).body || await (async () => { // Cast req to any to access potential body property
292 | const ct = contentType.parse(req.headers["content-type"] ?? "")
293 | if (ct.type !== "application/json") {
294 | throw new Error(`Unsupported content-type: ${ct.type}`)
295 | }
296 | const rawBody = await getRawBody(req, {
297 | limit: this._config.maxMessageSize,
298 | encoding: ct.parameters.charset ?? "utf-8"
299 | })
300 | const parsed = JSON.parse(rawBody.toString())
301 | logger.debug(`Received message: ${JSON.stringify(parsed)}`)
302 | return parsed
303 | })()
304 |
305 | const { id, method, params } = rawMessage
306 | logger.debug(`Parsed message - ID: ${id}, Method: ${method}`)
307 |
308 | const rpcMessage: JSONRPCMessage = {
309 | jsonrpc: "2.0",
310 | id: id,
311 | method: method,
312 | params: params
313 | }
314 |
315 | currentMessage = {
316 | id: id,
317 | method: method
318 | }
319 |
320 | logger.debug(`Processing RPC message: ${JSON.stringify({
321 | id: id,
322 | method: method,
323 | params: params
324 | })}`)
325 |
326 | if (!this._onmessage) {
327 | throw new Error("No message handler registered")
328 | }
329 |
330 | await this._onmessage(rpcMessage)
331 |
332 | res.writeHead(202).end("Accepted")
333 |
334 | logger.debug(`Successfully processed message ${rpcMessage.id}`)
335 |
336 | } catch (error: any) {
337 | const errorMessage = error instanceof Error ? error.message : String(error)
338 | logger.error(`Error handling message for session ${this._sessionId}:`)
339 | logger.error(`- Error: ${errorMessage}`)
340 | logger.error(`- Method: ${currentMessage.method || "unknown"}`)
341 | logger.error(`- Message ID: ${currentMessage.id || "unknown"}`)
342 |
343 | const errorResponse = {
344 | jsonrpc: "2.0",
345 | id: currentMessage.id || null,
346 | error: {
347 | code: -32000,
348 | message: errorMessage,
349 | data: {
350 | method: currentMessage.method || "unknown",
351 | sessionId: this._sessionId,
352 | connectionActive: Boolean(this._connections.size > 0),
353 | type: "message_handler_error"
354 | }
355 | }
356 | }
357 |
358 | res.writeHead(400).end(JSON.stringify(errorResponse))
359 | this._onerror?.(error as Error)
360 | }
361 | }
362 |
363 | // Broadcast message to all connected clients
364 | async send(message: JSONRPCMessage): Promise {
365 | if (this._connections.size === 0) {
366 | logger.warn("Attempted to send message, but no clients are connected.");
367 | // Optionally throw an error or just log
368 | // throw new Error("No SSE connections established");
369 | return;
370 | }
371 | const messageString = `data: ${JSON.stringify(message)}\n\n`;
372 | logger.debug(`Broadcasting message to ${this._connections.size} clients: ${JSON.stringify(message)}`);
373 | let failedSends = 0;
374 | for (const [connectionId, connection] of this._connections.entries()) {
375 | if (connection.res && !connection.res.writableEnded) {
376 | try {
377 | connection.res.write(messageString);
378 | }
379 | catch (error: any) {
380 | failedSends++;
381 | logger.error(`Error sending message to connection ${connectionId}: ${error instanceof Error ? error.message : String(error)}`);
382 | // Clean up the problematic connection
383 | this.cleanupConnection(connectionId);
384 | }
385 | }
386 | else {
387 | // Should not happen if cleanup is working, but handle defensively
388 | logger.warn(`Attempted to send to ended connection: ${connectionId}`);
389 | this.cleanupConnection(connectionId);
390 | }
391 | }
392 | if (failedSends > 0) {
393 | logger.warn(`Failed to send message to ${failedSends} connections.`);
394 | }
395 | }
396 |
397 | async close(): Promise {
398 | logger.info(`Closing SSE transport and ${this._connections.size} connections.`);
399 | // Close all active client connections
400 | for (const connectionId of this._connections.keys()) {
401 | this.cleanupConnection(connectionId, true); // Pass true to end the response
402 | }
403 | this._connections.clear(); // Ensure map is empty
404 | // Close the main server
405 | return new Promise((resolve) => {
406 | if (!this._server) {
407 | logger.debug("Server already stopped.");
408 | resolve();
409 | return;
410 | }
411 | this._server.close(() => {
412 | logger.info("SSE server stopped");
413 | this._server = undefined;
414 | this._onclose?.();
415 | resolve();
416 | });
417 | });
418 | }
419 |
420 | // Clean up a specific connection by its ID
421 | private cleanupConnection(connectionId: string, endResponse = false): void {
422 | const connection = this._connections.get(connectionId);
423 | if (connection) {
424 | logger.debug(`Cleaning up connection: ${connectionId}`);
425 | if (connection.intervalId) {
426 | clearInterval(connection.intervalId);
427 | }
428 | if (endResponse && connection.res && !connection.res.writableEnded) {
429 | try {
430 | connection.res.end();
431 | }
432 | catch (e: any) {
433 | logger.warn(`Error ending response for connection ${connectionId}: ${e instanceof Error ? e.message : String(e)}`);
434 | }
435 | }
436 | this._connections.delete(connectionId);
437 | logger.debug(`Connection removed: ${connectionId}. Remaining connections: ${this._connections.size}`);
438 | }
439 | else {
440 | logger.debug(`Attempted to clean up non-existent connection: ${connectionId}`);
441 | }
442 | }
443 |
444 | isRunning(): boolean {
445 | return Boolean(this._server)
446 | }
447 | }
448 |
--------------------------------------------------------------------------------
/src/transports/sse/types.ts:
--------------------------------------------------------------------------------
1 | import { AuthConfig } from "../../auth/types.js";
2 |
3 | /**
4 | * CORS configuration options for SSE transport
5 | */
6 | export interface CORSConfig {
7 | /**
8 | * Access-Control-Allow-Origin header
9 | * @default "*"
10 | */
11 | allowOrigin?: string;
12 |
13 | /**
14 | * Access-Control-Allow-Methods header
15 | * @default "GET, POST, OPTIONS"
16 | */
17 | allowMethods?: string;
18 |
19 | /**
20 | * Access-Control-Allow-Headers header
21 | * @default "Content-Type, Authorization, x-api-key"
22 | */
23 | allowHeaders?: string;
24 |
25 | /**
26 | * Access-Control-Expose-Headers header
27 | * @default "Content-Type, Authorization, x-api-key"
28 | */
29 | exposeHeaders?: string;
30 |
31 | /**
32 | * Access-Control-Max-Age header for preflight requests
33 | * @default "86400"
34 | */
35 | maxAge?: string;
36 | }
37 |
38 | /**
39 | * Configuration options for SSE transport
40 | */
41 | export interface SSETransportConfig {
42 | /**
43 | * Port to listen on
44 | */
45 | port?: number;
46 |
47 | /**
48 | * Endpoint for SSE events stream
49 | * @default "/sse"
50 | */
51 | endpoint?: string;
52 |
53 | /**
54 | * Endpoint for receiving messages via POST
55 | * @default "/messages"
56 | */
57 | messageEndpoint?: string;
58 |
59 | /**
60 | * Maximum allowed message size in bytes
61 | * @default "4mb"
62 | */
63 | maxMessageSize?: string;
64 |
65 | /**
66 | * Custom headers to add to SSE responses
67 | */
68 | headers?: Record;
69 |
70 | /**
71 | * CORS configuration
72 | */
73 | cors?: CORSConfig;
74 |
75 | /**
76 | * Authentication configuration
77 | */
78 | auth?: AuthConfig;
79 | }
80 |
81 | /**
82 | * Internal configuration type with required fields except headers
83 | */
84 | export type SSETransportConfigInternal = Required> & {
85 | headers?: Record;
86 | auth?: AuthConfig;
87 | cors?: CORSConfig;
88 | };
89 |
90 | /**
91 | * Default CORS configuration
92 | */
93 | export const DEFAULT_CORS_CONFIG: CORSConfig = {
94 | allowOrigin: "*",
95 | allowMethods: "GET, POST, DELETE, OPTIONS",
96 | allowHeaders: "Content-Type, Accept, Authorization, x-api-key, Mcp-Session-Id, Last-Event-ID",
97 | exposeHeaders: "Content-Type, Authorization, x-api-key, Mcp-Session-Id",
98 | maxAge: "86400"
99 | };
100 |
101 | /**
102 | * Default configuration values
103 | */
104 | export const DEFAULT_SSE_CONFIG: SSETransportConfigInternal = {
105 | port: 8080,
106 | endpoint: "/sse",
107 | messageEndpoint: "/messages",
108 | maxMessageSize: "4mb"
109 | };
110 |
--------------------------------------------------------------------------------
/src/transports/stdio/server.ts:
--------------------------------------------------------------------------------
1 | import { StdioServerTransport as SDKStdioTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2 | import { BaseTransport } from "../base.js";
3 | import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
4 | import {
5 | ImageTransportOptions,
6 | DEFAULT_IMAGE_OPTIONS,
7 | hasImageContent,
8 | prepareImageForTransport,
9 | ImageContent
10 | } from "../utils/image-handler.js";
11 | import { logger } from "../../core/Logger.js";
12 |
13 | type ExtendedJSONRPCMessage = JSONRPCMessage & {
14 | result?: {
15 | content?: Array;
16 | [key: string]: unknown;
17 | };
18 | };
19 |
20 | /**
21 | * StdioServerTransport
22 | */
23 | export class StdioServerTransport implements BaseTransport {
24 | readonly type = "stdio";
25 | private transport: SDKStdioTransport;
26 | private running: boolean = false;
27 | private imageOptions: ImageTransportOptions;
28 |
29 | constructor(imageOptions: Partial = {}) {
30 | this.transport = new SDKStdioTransport();
31 | this.imageOptions = {
32 | ...DEFAULT_IMAGE_OPTIONS,
33 | ...imageOptions
34 | };
35 | }
36 |
37 | async start(): Promise {
38 | await this.transport.start();
39 | this.running = true;
40 | }
41 |
42 | async send(message: ExtendedJSONRPCMessage): Promise {
43 | try {
44 | if (hasImageContent(message)) {
45 | message = this.prepareMessageWithImage(message);
46 | }
47 | await this.transport.send(message);
48 | } catch (error) {
49 | logger.error(`Error sending message through stdio transport: ${error}`);
50 | throw error;
51 | }
52 | }
53 |
54 | private prepareMessageWithImage(message: ExtendedJSONRPCMessage): ExtendedJSONRPCMessage {
55 | if (!message.result?.content) {
56 | return message;
57 | }
58 |
59 | const processedContent = message.result.content.map((item: ImageContent | { type: string; [key: string]: unknown }) => {
60 | if (item.type === 'image') {
61 | return prepareImageForTransport(item as ImageContent, this.imageOptions);
62 | }
63 | return item;
64 | });
65 |
66 | return {
67 | ...message,
68 | result: {
69 | ...message.result,
70 | content: processedContent
71 | }
72 | };
73 | }
74 |
75 | async close(): Promise {
76 | await this.transport.close();
77 | this.running = false;
78 | }
79 |
80 | isRunning(): boolean {
81 | return this.running;
82 | }
83 |
84 | set onclose(handler: (() => void) | undefined) {
85 | this.transport.onclose = handler;
86 | }
87 |
88 | set onerror(handler: ((error: Error) => void) | undefined) {
89 | this.transport.onerror = handler;
90 | }
91 |
92 | set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) {
93 | this.transport.onmessage = handler;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/transports/utils/image-handler.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | /**
4 | * Configuration options for image transport
5 | */
6 | export interface ImageTransportOptions {
7 | maxSize: number;
8 | allowedMimeTypes: string[];
9 | compressionQuality?: number;
10 | }
11 |
12 | /**
13 | * Schema for image content validation
14 | */
15 | export const ImageContentSchema = z.object({
16 | type: z.literal("image"),
17 | data: z.string(),
18 | mimeType: z.string()
19 | });
20 |
21 | export type ImageContent = z.infer;
22 |
23 | /**
24 | * Default configuration for image transport
25 | */
26 | export const DEFAULT_IMAGE_OPTIONS: ImageTransportOptions = {
27 | maxSize: 5 * 1024 * 1024, // 5MB
28 | allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
29 | compressionQuality: 0.8
30 | };
31 |
32 | /**
33 | * Validates image content against transport options
34 | */
35 | export function validateImageTransport(content: ImageContent, options: ImageTransportOptions = DEFAULT_IMAGE_OPTIONS): void {
36 | // Validate schema
37 | ImageContentSchema.parse(content);
38 |
39 | // Validate MIME type
40 | if (!options.allowedMimeTypes.includes(content.mimeType)) {
41 | throw new Error(`Unsupported image type: ${content.mimeType}. Allowed types: ${options.allowedMimeTypes.join(', ')}`);
42 | }
43 |
44 | // Validate base64 format
45 | if (!isBase64(content.data)) {
46 | throw new Error('Invalid base64 image data');
47 | }
48 |
49 | // Validate size
50 | const sizeInBytes = Buffer.from(content.data, 'base64').length;
51 | if (sizeInBytes > options.maxSize) {
52 | throw new Error(`Image size ${sizeInBytes} bytes exceeds maximum allowed size of ${options.maxSize} bytes`);
53 | }
54 | }
55 |
56 | /**
57 | * Prepares image content for transport
58 | * This function can be extended to handle compression, format conversion, etc.
59 | */
60 | export function prepareImageForTransport(content: ImageContent, options: ImageTransportOptions = DEFAULT_IMAGE_OPTIONS): ImageContent {
61 | validateImageTransport(content, options);
62 |
63 | // For now, we just return the validated content
64 | // Future: implement compression, format conversion, etc.
65 | return content;
66 | }
67 |
68 | /**
69 | * Checks if a string is valid base64
70 | */
71 | function isBase64(str: string): boolean {
72 | if (str === '' || str.trim() === '') {
73 | return false;
74 | }
75 | try {
76 | return btoa(atob(str)) === str;
77 | } catch (_error) {
78 | return false;
79 | }
80 | }
81 |
82 | /**
83 | * Gets the size of a base64 image in bytes
84 | */
85 | export function getBase64ImageSize(base64String: string): number {
86 | return Buffer.from(base64String, 'base64').length;
87 | }
88 |
89 | /**
90 | * Utility type for messages containing image content
91 | */
92 | export type MessageWithImage = {
93 | result?: {
94 | content?: Array;
95 | };
96 | [key: string]: unknown;
97 | };
98 |
99 | /**
100 | * Checks if a message contains image content
101 | */
102 | export function hasImageContent(message: unknown): message is MessageWithImage {
103 | if (typeof message !== 'object' || message === null) {
104 | return false;
105 | }
106 |
107 | const msg = message as MessageWithImage;
108 | return Array.isArray(msg.result?.content) &&
109 | msg.result.content.some(item => item.type === 'image');
110 | }
111 |
--------------------------------------------------------------------------------
/src/transports/utils/ping-message.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Pre-stringified JSON-RPC ping message formatted for Server-Sent Events (SSE).
3 | * Includes the 'data: ' prefix and trailing newlines.
4 | */
5 | export const PING_SSE_MESSAGE = 'data: {"jsonrpc":"2.0","method":"ping"}\n\n';
--------------------------------------------------------------------------------
/src/utils/headers.ts:
--------------------------------------------------------------------------------
1 | import { ServerResponse } from "node:http"
2 |
3 | export function getRequestHeader(headers: NodeJS.Dict, headerName: string): string | undefined {
4 | const headerLower = headerName.toLowerCase()
5 | return Object.entries(headers).find(
6 | ([key]) => key.toLowerCase() === headerLower
7 | )?.[1] as string | undefined
8 | }
9 |
10 | export function setResponseHeaders(res: ServerResponse, headers: Record): void {
11 | Object.entries(headers).forEach(([key, value]) => {
12 | res.setHeader(key, value)
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "declaration": true,
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "resolveJsonModule": true
14 | },
15 | "include": ["src/**/*"],
16 | "exclude": ["node_modules", "**/*.test.ts"]
17 | }
18 |
--------------------------------------------------------------------------------