@novorb/mcp-core 2.1.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +77 -163
  2. package/bin/mcp-core-jest.mjs +112 -0
  3. package/dist/auth.d.ts +57 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +74 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/bootstrap.d.ts +129 -0
  8. package/dist/bootstrap.d.ts.map +1 -0
  9. package/dist/bootstrap.js +206 -0
  10. package/dist/bootstrap.js.map +1 -0
  11. package/dist/client-input.d.ts +72 -0
  12. package/dist/client-input.d.ts.map +1 -0
  13. package/dist/client-input.js +109 -0
  14. package/dist/client-input.js.map +1 -0
  15. package/dist/credential-checks.d.ts +80 -0
  16. package/dist/credential-checks.d.ts.map +1 -0
  17. package/dist/credential-checks.js +189 -0
  18. package/dist/credential-checks.js.map +1 -0
  19. package/dist/credential-health.d.ts +12 -0
  20. package/dist/credential-health.d.ts.map +1 -0
  21. package/dist/credential-health.js +12 -0
  22. package/dist/credential-health.js.map +1 -0
  23. package/dist/credential-report.d.ts +46 -0
  24. package/dist/credential-report.d.ts.map +1 -0
  25. package/dist/credential-report.js +135 -0
  26. package/dist/credential-report.js.map +1 -0
  27. package/dist/credential-status-tool.d.ts +20 -0
  28. package/dist/credential-status-tool.d.ts.map +1 -0
  29. package/dist/credential-status-tool.js +33 -0
  30. package/dist/credential-status-tool.js.map +1 -0
  31. package/dist/credentials.d.ts +288 -0
  32. package/dist/credentials.d.ts.map +1 -0
  33. package/dist/credentials.js +652 -0
  34. package/dist/credentials.js.map +1 -0
  35. package/dist/express-peers.d.ts +29 -0
  36. package/dist/express-peers.d.ts.map +1 -0
  37. package/dist/express-peers.js +36 -0
  38. package/dist/express-peers.js.map +1 -0
  39. package/dist/express.d.ts +74 -0
  40. package/dist/express.d.ts.map +1 -0
  41. package/dist/express.js +164 -0
  42. package/dist/express.js.map +1 -0
  43. package/dist/http-client.d.ts +277 -42
  44. package/dist/http-client.d.ts.map +1 -1
  45. package/dist/http-client.js +620 -180
  46. package/dist/http-client.js.map +1 -1
  47. package/dist/index.d.ts +27 -6
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +47 -8
  50. package/dist/index.js.map +1 -1
  51. package/dist/limits.d.ts +28 -0
  52. package/dist/limits.d.ts.map +1 -0
  53. package/dist/limits.js +37 -0
  54. package/dist/limits.js.map +1 -0
  55. package/dist/list-pagination.d.ts +41 -0
  56. package/dist/list-pagination.d.ts.map +1 -0
  57. package/dist/list-pagination.js +104 -0
  58. package/dist/list-pagination.js.map +1 -0
  59. package/dist/net.d.ts +112 -0
  60. package/dist/net.d.ts.map +1 -0
  61. package/dist/net.js +490 -0
  62. package/dist/net.js.map +1 -0
  63. package/dist/notifications.d.ts +78 -0
  64. package/dist/notifications.d.ts.map +1 -0
  65. package/dist/notifications.js +173 -0
  66. package/dist/notifications.js.map +1 -0
  67. package/dist/pagination.d.ts +236 -5
  68. package/dist/pagination.d.ts.map +1 -1
  69. package/dist/pagination.js +393 -6
  70. package/dist/pagination.js.map +1 -1
  71. package/dist/prompts.d.ts +70 -0
  72. package/dist/prompts.d.ts.map +1 -0
  73. package/dist/prompts.js +109 -0
  74. package/dist/prompts.js.map +1 -0
  75. package/dist/protocol-errors.d.ts +15 -0
  76. package/dist/protocol-errors.d.ts.map +1 -0
  77. package/dist/protocol-errors.js +28 -0
  78. package/dist/protocol-errors.js.map +1 -0
  79. package/dist/protocol-logging.d.ts +37 -0
  80. package/dist/protocol-logging.d.ts.map +1 -0
  81. package/dist/protocol-logging.js +77 -0
  82. package/dist/protocol-logging.js.map +1 -0
  83. package/dist/rate-limiter.d.ts +36 -0
  84. package/dist/rate-limiter.d.ts.map +1 -0
  85. package/dist/rate-limiter.js +79 -0
  86. package/dist/rate-limiter.js.map +1 -0
  87. package/dist/request-state.d.ts +74 -0
  88. package/dist/request-state.d.ts.map +1 -0
  89. package/dist/request-state.js +190 -0
  90. package/dist/request-state.js.map +1 -0
  91. package/dist/resources.d.ts +181 -0
  92. package/dist/resources.d.ts.map +1 -0
  93. package/dist/resources.js +302 -0
  94. package/dist/resources.js.map +1 -0
  95. package/dist/result-limits.d.ts +80 -0
  96. package/dist/result-limits.d.ts.map +1 -0
  97. package/dist/result-limits.js +248 -0
  98. package/dist/result-limits.js.map +1 -0
  99. package/dist/retry.d.ts +79 -23
  100. package/dist/retry.d.ts.map +1 -1
  101. package/dist/retry.js +170 -57
  102. package/dist/retry.js.map +1 -1
  103. package/dist/sdk/client-stdio.d.ts +6 -0
  104. package/dist/sdk/client-stdio.d.ts.map +1 -0
  105. package/dist/sdk/client-stdio.js +6 -0
  106. package/dist/sdk/client-stdio.js.map +1 -0
  107. package/dist/sdk/client.d.ts +6 -0
  108. package/dist/sdk/client.d.ts.map +1 -0
  109. package/dist/sdk/client.js +6 -0
  110. package/dist/sdk/client.js.map +1 -0
  111. package/dist/sdk/express.d.ts +3 -0
  112. package/dist/sdk/express.d.ts.map +1 -0
  113. package/dist/sdk/express.js +11 -0
  114. package/dist/sdk/express.js.map +1 -0
  115. package/dist/sdk/node.d.ts +6 -0
  116. package/dist/sdk/node.d.ts.map +1 -0
  117. package/dist/sdk/node.js +6 -0
  118. package/dist/sdk/node.js.map +1 -0
  119. package/dist/sdk/server-stdio.d.ts +6 -0
  120. package/dist/sdk/server-stdio.d.ts.map +1 -0
  121. package/dist/sdk/server-stdio.js +6 -0
  122. package/dist/sdk/server-stdio.js.map +1 -0
  123. package/dist/sdk/server.d.ts +6 -0
  124. package/dist/sdk/server.d.ts.map +1 -0
  125. package/dist/sdk/server.js +6 -0
  126. package/dist/sdk/server.js.map +1 -0
  127. package/dist/server.d.ts +88 -35
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +98 -44
  130. package/dist/server.js.map +1 -1
  131. package/dist/task-request.d.ts +20 -0
  132. package/dist/task-request.d.ts.map +1 -0
  133. package/dist/task-request.js +43 -0
  134. package/dist/task-request.js.map +1 -0
  135. package/dist/task-store.d.ts +230 -0
  136. package/dist/task-store.d.ts.map +1 -0
  137. package/dist/task-store.js +459 -0
  138. package/dist/task-store.js.map +1 -0
  139. package/dist/task-tools.d.ts +115 -0
  140. package/dist/task-tools.d.ts.map +1 -0
  141. package/dist/task-tools.js +397 -0
  142. package/dist/task-tools.js.map +1 -0
  143. package/dist/testing/client.d.ts +25 -0
  144. package/dist/testing/client.d.ts.map +1 -0
  145. package/dist/testing/client.js +11 -0
  146. package/dist/testing/client.js.map +1 -0
  147. package/dist/testing/in-memory.d.ts +26 -0
  148. package/dist/testing/in-memory.d.ts.map +1 -0
  149. package/dist/testing/in-memory.js +41 -0
  150. package/dist/testing/in-memory.js.map +1 -0
  151. package/dist/testing/index.d.ts +20 -0
  152. package/dist/testing/index.d.ts.map +1 -0
  153. package/dist/testing/index.js +20 -0
  154. package/dist/testing/index.js.map +1 -0
  155. package/dist/testing/isolated-dirs.d.ts +27 -0
  156. package/dist/testing/isolated-dirs.d.ts.map +1 -0
  157. package/dist/testing/isolated-dirs.js +67 -0
  158. package/dist/testing/isolated-dirs.js.map +1 -0
  159. package/dist/testing/logs.d.ts +61 -0
  160. package/dist/testing/logs.d.ts.map +1 -0
  161. package/dist/testing/logs.js +113 -0
  162. package/dist/testing/logs.js.map +1 -0
  163. package/dist/testing/raw-http.d.ts +25 -0
  164. package/dist/testing/raw-http.d.ts.map +1 -0
  165. package/dist/testing/raw-http.js +41 -0
  166. package/dist/testing/raw-http.js.map +1 -0
  167. package/dist/testing/raw-peer.d.ts +43 -0
  168. package/dist/testing/raw-peer.d.ts.map +1 -0
  169. package/dist/testing/raw-peer.js +101 -0
  170. package/dist/testing/raw-peer.js.map +1 -0
  171. package/dist/testing/recording-server.d.ts +38 -0
  172. package/dist/testing/recording-server.d.ts.map +1 -0
  173. package/dist/testing/recording-server.js +38 -0
  174. package/dist/testing/recording-server.js.map +1 -0
  175. package/dist/testing/server-process.d.ts +76 -0
  176. package/dist/testing/server-process.d.ts.map +1 -0
  177. package/dist/testing/server-process.js +241 -0
  178. package/dist/testing/server-process.js.map +1 -0
  179. package/dist/testing/tools.d.ts +53 -0
  180. package/dist/testing/tools.d.ts.map +1 -0
  181. package/dist/testing/tools.js +98 -0
  182. package/dist/testing/tools.js.map +1 -0
  183. package/dist/testing/unit.d.ts +52 -0
  184. package/dist/testing/unit.d.ts.map +1 -0
  185. package/dist/testing/unit.js +95 -0
  186. package/dist/testing/unit.js.map +1 -0
  187. package/dist/tools.d.ts +205 -0
  188. package/dist/tools.d.ts.map +1 -0
  189. package/dist/tools.js +370 -0
  190. package/dist/tools.js.map +1 -0
  191. package/dist/transport.d.ts +143 -52
  192. package/dist/transport.d.ts.map +1 -1
  193. package/dist/transport.js +292 -123
  194. package/dist/transport.js.map +1 -1
  195. package/dist/url-path.d.ts +49 -0
  196. package/dist/url-path.d.ts.map +1 -0
  197. package/dist/url-path.js +165 -0
  198. package/dist/url-path.js.map +1 -0
  199. package/dist/utils/dataStore.d.ts +143 -0
  200. package/dist/utils/dataStore.d.ts.map +1 -0
  201. package/dist/utils/dataStore.js +429 -0
  202. package/dist/utils/dataStore.js.map +1 -0
  203. package/dist/utils/envLoader.d.ts +129 -0
  204. package/dist/utils/envLoader.d.ts.map +1 -0
  205. package/dist/utils/envLoader.js +399 -0
  206. package/dist/utils/envLoader.js.map +1 -0
  207. package/dist/utils/logger.d.ts +181 -0
  208. package/dist/utils/logger.d.ts.map +1 -0
  209. package/dist/utils/logger.js +664 -0
  210. package/dist/utils/logger.js.map +1 -0
  211. package/dist/utils/redact.d.ts +50 -0
  212. package/dist/utils/redact.d.ts.map +1 -0
  213. package/dist/utils/redact.js +318 -0
  214. package/dist/utils/redact.js.map +1 -0
  215. package/docs/api-reference.md +29 -0
  216. package/docs/building-a-server.md +63 -0
  217. package/docs/credentials.md +131 -0
  218. package/docs/data-store.md +37 -0
  219. package/docs/env.md +38 -0
  220. package/docs/features.md +28 -0
  221. package/docs/http-client.md +116 -0
  222. package/docs/logger.md +100 -0
  223. package/docs/migrating-to-v3.md +45 -0
  224. package/docs/outbound-urls.md +104 -0
  225. package/docs/pagination.md +45 -0
  226. package/docs/protocol-features.md +62 -0
  227. package/docs/resources-prompts-tasks.md +95 -0
  228. package/docs/sdk.md +33 -0
  229. package/docs/server.md +68 -0
  230. package/docs/testing-harness.md +139 -0
  231. package/docs/testing.md +151 -0
  232. package/docs/tools.md +88 -0
  233. package/docs/transport.md +85 -0
  234. package/examples/testing/tools.integration.test.example.ts +312 -0
  235. package/examples/testing/tools.unit.test.example.ts +149 -0
  236. package/package.json +130 -17
  237. package/dist/logger.d.ts +0 -80
  238. package/dist/logger.d.ts.map +0 -1
  239. package/dist/logger.js +0 -237
  240. package/dist/logger.js.map +0 -1
package/README.md CHANGED
@@ -4,202 +4,116 @@
4
4
 
5
5
  [![CI](https://github.com/Novorb/mcp-core/actions/workflows/ci.yml/badge.svg)](https://github.com/Novorb/mcp-core/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@novorb/mcp-core)](https://www.npmjs.com/package/@novorb/mcp-core)
7
- [![Node](https://img.shields.io/badge/Node-22%2B-green)](https://nodejs.org/)
8
- [![Tests](https://img.shields.io/badge/Tests-128-purple)]()
7
+ [![Node](https://img.shields.io/badge/Node-20%2B-green)](https://nodejs.org/)
8
+ [![Tests](https://img.shields.io/badge/Tests-1186-purple)]()
9
9
  [![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
10
10
 
11
11
  #### Quality Assurance
12
12
 
13
- [![Coverage](https://img.shields.io/badge/coverage-93.46%25%20lines-brightgreen)]()
14
- [![Audit](https://img.shields.io/badge/audit-0_vulns-brightgreen)]()
13
+ [![Coverage](https://img.shields.io/badge/coverage-98%25%20lines-brightgreen)]()
14
+ [![Audit](https://img.shields.io/badge/audit-1_moderate-yellow)](docs/sdk.md#known-advisories)
15
15
  [![Fallow](https://img.shields.io/badge/fallow_health-86.6_(A)-brightgreen)](https://fallow.tools)
16
16
  [![Maintainability](https://img.shields.io/badge/maintainability-94-brightgreen)](https://fallow.tools)
17
17
 
18
- Shared TypeScript utilities for building MCP (Model Context Protocol) servers — logging, HTTP clients, retry logic, pagination, and transport — so every MCP server gets a consistent, tested foundation without reimplementing common patterns.
18
+ Shared TypeScript foundation for building MCP (Model Context Protocol) servers: bootstrap, transport, tools, logging, HTTP clients, credentials and testing, so every server gets the same tested behaviour without reimplementing it. Version 3 is built on the MCP SDK v2 packages.
19
19
 
20
20
  ## Features
21
21
 
22
- - **Structured logging** — file output with rotation, stderr output, text or JSON format, secret redaction
23
- - **HTTP client** — axios wrapper with automatic retry on 429/5xx, request logging, pagination
24
- - **Retry with backoff** — exponential backoff with jitter, configurable retries and delays
25
- - **Pagination helpers** — normalize params, compute metadata, paginate arrays in memory
26
- - **Unified transport** — stdio and stateless streamable-HTTP with built-in auth and DNS-rebinding protection
27
- - **Server builder** — `McpServer` factory, annotation presets, and SDK re-exports from one import point
28
- - **ESM native** — built as ES modules with full TypeScript declarations
22
+ - **Bootstrap** (`runMcpServer`): one entry point for every server, from env loading to clean shutdown
23
+ - **Transport**: stdio and stateless streamable HTTP with bearer auth, plus an adapter for existing Express apps
24
+ - **Tools** (`registerTools`): central logging, safe error mapping, result caps and tamper-checked cursors
25
+ - **Protocol features**: logging, elicitation, sampling, roots, progress and cancellation across both protocol eras
26
+ - **Resources, prompts and notifications**: registration helpers, runtime toggles and change events on every transport
27
+ - **Tasks and requestState**: long-running tools with file or in-memory stores, and sealed multi-step state
28
+ - **HTTP client, retry and pagination**: one hardened client per upstream, one retry policy, page walkers for common API shapes
29
+ - **Guarded outbound URLs**: safe fetches of caller-supplied URLs and safe encoding of path segments
30
+ - **Credential health**: startup checks, key pools with rotation, and a report on every result
31
+ - **Env and data store**: one env loader with typed validation, and traversal-safe, atomic disk writes
32
+ - **Logger**: console and rotating file sinks with secret redaction and PII masking
33
+ - **Testing harness**: unit and integration helpers for your own server, and a cross-platform jest runner
34
+ - **The SDK through mcp-core**: a curated entry plus full re-exports, so servers depend on one package
29
35
 
30
36
  ## Documentation
31
37
 
32
- - **[CLAUDE.md](CLAUDE.md)** — agent/contributor context and working conventions
33
- - **[docs/TESTING.md](docs/TESTING.md)** — test runner commands, conventions, and guidelines
38
+ **Getting started**
34
39
 
35
- ## Installation
40
+ - [Building a server](docs/building-a-server.md): a minimal server and what `runMcpServer` does
41
+ - [Features](docs/features.md): the full feature list in detail
42
+ - [API reference](docs/api-reference.md): every import path and what it exports
43
+ - [The MCP SDK](docs/sdk.md): importing the SDK through mcp-core, and what SDK v2 changes
44
+ - [Migrating to 3.0](docs/migrating-to-v3.md): breaking changes and moving a low-level `Server` consumer
36
45
 
37
- ```bash
38
- pnpm add @novorb/mcp-core
39
- ```
46
+ **Serving**
40
47
 
41
- ## Modules
48
+ - [Server](docs/server.md): the server builder, annotation presets, list pagination
49
+ - [Transport and Express adapter](docs/transport.md): stdio, HTTP, auth and guards
50
+ - [Tools](docs/tools.md): definitions, response limits, cursors
51
+ - [Protocol features](docs/protocol-features.md): logging, elicitation, sampling, roots, progress, cancellation
52
+ - [Resources, prompts, notifications, tasks, requestState](docs/resources-prompts-tasks.md)
42
53
 
43
- ### Logger
54
+ **Upstreams and configuration**
44
55
 
45
- Unified logger supporting file output (with rotation) and stderr output, in text or structured JSON format. MCP servers use stdio for protocol communication, so logging to stdout/stderr directly isn't always possible — file-based logging solves this. Sensitive-keyed fields in `meta` are redacted before serialisation.
56
+ - [HTTP client and retry](docs/http-client.md)
57
+ - [Pagination](docs/pagination.md)
58
+ - [Outbound URLs](docs/outbound-urls.md): `guardedFetch` and URL path segments
59
+ - [Env](docs/env.md)
60
+ - [Credential health](docs/credentials.md)
61
+ - [Data store](docs/data-store.md)
62
+ - [Logger](docs/logger.md)
46
63
 
47
- ```typescript
48
- import { initLogger, LogLevel } from '@novorb/mcp-core';
64
+ **Testing and contributing**
49
65
 
50
- // File-based (writes to logs/mcp.log with rotation)
51
- const logger = initLogger({
52
- output: 'file',
53
- logDir: './logs',
54
- level: LogLevel.INFO,
55
- });
66
+ - [Testing your server](docs/testing-harness.md): the `./testing` harness and `mcp-core-jest`
67
+ - [Testing mcp-core](docs/testing.md): this repo's test commands and conventions
68
+ - [CLAUDE.md](CLAUDE.md): agent and contributor context
56
69
 
57
- // Stderr with structured JSON
58
- const logger = initLogger({
59
- output: 'stderr',
60
- format: 'json',
61
- });
62
-
63
- logger.info('Server started', 'Server');
64
- logger.error('Request failed', 'HttpClient', { status: 500, path: '/api/items' });
65
- ```
66
-
67
- ### HTTP Client
68
-
69
- Pre-configured axios wrapper with automatic retry on transient failures (429, 5xx, network errors) for idempotent methods, request/response logging, and pagination support via `X-Total-Count` headers.
70
-
71
- ```typescript
72
- import { createHttpClient } from '@novorb/mcp-core';
73
-
74
- const client = createHttpClient({
75
- baseURL: 'https://api.example.com',
76
- headers: { 'Authorization': 'Bearer token' },
77
- retries: 3,
78
- logContext: 'MyApiClient',
79
- });
70
+ ## Quick Start
80
71
 
81
- const item = await client.get('/items/1');
82
- const { data, totalCount } = await client.getPaginated('/items', { page: 1, per_page: 50 });
83
- await client.post('/items', { name: 'new item' });
72
+ ```bash
73
+ pnpm add @novorb/mcp-core zod
84
74
  ```
85
75
 
86
- ### Retry
87
-
88
- Exponential backoff with jitter for retrying failed operations. Used internally by the HTTP client, but also available standalone.
89
-
90
76
  ```typescript
91
- import { retryWithBackoff, isRetryableError } from '@novorb/mcp-core';
92
-
93
- const result = await retryWithBackoff(async (ctx) => {
94
- console.log(`Attempt ${ctx.attempt + 1}/${ctx.maxRetries + 1}`);
95
- return await someUnreliableOperation();
96
- }, {
97
- maxRetries: 3,
98
- initialDelayMs: 1000,
99
- backoffMultiplier: 2,
77
+ import { z } from 'zod';
78
+ import { runMcpServer } from '@novorb/mcp-core/bootstrap';
79
+ import { packageRootFrom } from '@novorb/mcp-core/logger';
80
+ import { TOOL_ANNOTATIONS } from '@novorb/mcp-core/server';
81
+ import { defineTool, registerTools, toolResult } from '@novorb/mcp-core/tools';
82
+
83
+ await runMcpServer({
84
+ rootDir: packageRootFrom(import.meta.url),
85
+ env: [],
86
+ create: (_shared, _req, { env, info, createServer }) => {
87
+ const server = createServer();
88
+ registerTools(
89
+ server,
90
+ [
91
+ defineTool({
92
+ name: 'hello',
93
+ description: 'Say hello',
94
+ inputSchema: z.object({ name: z.string() }),
95
+ annotations: TOOL_ANNOTATIONS.readOnly,
96
+ handler: async ({ name }) => toolResult(`Hello ${name}`),
97
+ }),
98
+ ],
99
+ { env, serverName: info.name },
100
+ );
101
+ return server;
102
+ },
100
103
  });
101
104
  ```
102
105
 
103
- ### Pagination
104
-
105
- Normalize pagination parameters, compute metadata, and paginate arrays in memory.
106
-
107
- ```typescript
108
- import { normalizePagination, createPaginationMetadata, paginateArray } from '@novorb/mcp-core';
109
-
110
- const params = normalizePagination({ page: 3, per_page: 200 });
111
- // { page: 3, per_page: 100 } (capped at max)
112
-
113
- const metadata = createPaginationMetadata(500, params);
114
- // { totalCount: 500, page: 3, perPage: 100, totalPages: 5 }
115
-
116
- const { data, metadata } = paginateArray(allItems, { page: 2, per_page: 20 });
117
- ```
118
-
119
- ### Transport
120
-
121
- Unified transport that selects between stdio and stateless streamable HTTP based on the `MCP_TRANSPORT` environment variable. HTTP mode is fail-closed (refuses to start without auth unless explicitly disabled) and supports Bearer-token auth, body-size caps, and opt-in DNS-rebinding protection.
122
-
123
- ```typescript
124
- import { startMcpServer } from '@novorb/mcp-core/transport';
125
-
126
- // Factory (recommended for HTTP — fresh server instance per request):
127
- await startMcpServer(() => createServer(services));
128
-
129
- // Server instance (fine for stdio):
130
- await startMcpServer(server);
131
- ```
132
-
133
- | Variable | Default | Description |
134
- |----------|---------|-------------|
135
- | `MCP_TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
136
- | `PORT` | `8080` | HTTP listen port |
137
- | `MCP_HTTP_HOST` | `0.0.0.0` | Bind address (`127.0.0.1` for local-only) |
138
- | `MCP_API_KEY` | none | Bearer token for HTTP auth (required in HTTP mode unless disabled) |
139
- | `MCP_AUTH_DISABLED` | `false` | Set `true` to run HTTP without auth (only behind a fronting gateway) |
140
- | `MCP_MAX_BODY_BYTES` | 4 MiB | Max accepted POST body size |
141
- | `MCP_ALLOWED_HOSTS` | none | Comma-separated Host allowlist (enables DNS-rebinding protection) |
142
- | `MCP_ALLOWED_ORIGINS` | none | Comma-separated Origin allowlist (enables DNS-rebinding protection) |
143
-
144
- ### Server
145
-
146
- Import MCP server-building primitives from here instead of the SDK directly, to centralise the MCP dependency.
147
-
148
- ```typescript
149
- import { createMcpServer, TOOL_ANNOTATIONS, completable } from '@novorb/mcp-core/server';
150
- import type { McpToolAnnotations, McpCallToolResult } from '@novorb/mcp-core/server';
151
-
152
- const server = createMcpServer('my-mcp', '1.0.0');
153
-
154
- server.registerTool('lookup', {
155
- description: 'Look up a value',
156
- inputSchema: { key: z.string() },
157
- outputSchema: { value: z.string(), found: z.boolean() },
158
- annotations: TOOL_ANNOTATIONS.readOnly,
159
- }, async ({ key }) => ({
160
- content: [{ type: 'text', text: value }],
161
- structuredContent: { value, found: true },
162
- }));
163
- ```
164
-
165
- **`TOOL_ANNOTATIONS` presets:** `readOnly` (lookups/searches), `idempotentWrite` (upserts), `mutableWrite` (non-idempotent writes), `destructive` (deletes/overwrites).
166
-
167
- ## Testing
168
-
169
- ```bash
170
- pnpm test # 122 tests
171
- pnpm run test:watch # watch mode
172
- pnpm run test:coverage
173
- ```
174
-
175
- > **Windows note:** the test scripts set `NODE_OPTIONS=` inline, which is Unix shell syntax. On Windows, invoke jest directly via a bash shell. See [docs/TESTING.md](docs/TESTING.md).
176
-
177
- ## Building
178
-
179
- ```bash
180
- pnpm run build # tsc → dist/
181
- ```
182
-
183
- This is an ESM package (`"type": "module"`). `tsconfig.json` is self-contained so it builds standalone.
184
-
185
- ## CI / Releasing
186
-
187
- GitHub Actions (`.github/workflows/`):
188
-
189
- - **CI** (`ci.yml`) — runs build + test on every push to `main` and on pull requests.
190
- - **Publish** (`publish.yml`) — on a pushed version tag `v*`, runs build + test, verifies the
191
- tag matches `package.json` `version`, then publishes to the public npm registry (with provenance).
106
+ The server runs on stdio by default; set `MCP_TRANSPORT=http` for HTTP. Continue with [Building a server](docs/building-a-server.md).
192
107
 
193
- Release flow:
108
+ ## Development
194
109
 
195
110
  ```bash
196
- # 1. bump "version" in package.json (e.g. 2.1.1), commit
197
- # 2. tag and push — the tag triggers the publish workflow
198
- git tag v2.1.1 && git push origin v2.1.1
111
+ pnpm run build # tsc to dist/
112
+ pnpm test # all suites
113
+ pnpm run qa:all # full QA matrix
199
114
  ```
200
115
 
201
- Requires the repo secret **`NPM_TOKEN`** (an npm automation/granular token with publish rights for
202
- the `@novorb` scope) under *Settings → Secrets and variables → Actions*.
116
+ This is an ESM package (`"type": "module"`). See [Testing mcp-core](docs/testing.md).
203
117
 
204
118
  ## License
205
119
 
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp-core-jest: a cross-platform jest launcher for ESM MCP servers (and for mcp-core itself).
4
+ *
5
+ * ESM jest suites (`"type": "module"` + ts-jest useESM) need the node binary started with
6
+ * `--experimental-vm-modules`. The usual `NODE_OPTIONS=--experimental-vm-modules jest` script is POSIX
7
+ * inline-env syntax and does not run under PowerShell or cmd. This launcher starts node with the flag
8
+ * and runs the CALLER's jest: jest is resolved from the current working directory (the consumer's
9
+ * package), so it stays the consumer's devDependency and mcp-core never depends on it at runtime.
10
+ *
11
+ * "test:unit": "mcp-core-jest tests/unit",
12
+ * "test:integration": "mcp-core-jest tests/integration --runInBand --coverage=false"
13
+ *
14
+ * Every argument is passed through to jest unchanged. The exit code is jest's; a signal that ends jest
15
+ * ends this process with the same signal; SIGTERM/SIGHUP are forwarded on POSIX and SIGINT (which reaches jest
16
+ * from the terminal) is waited out, never doubled.
17
+ */
18
+ import { spawn } from 'node:child_process';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { createRequire } from 'node:module';
21
+ import { dirname, join, resolve } from 'node:path';
22
+
23
+ /** The directory of the `jest` package that owns `entry` (the nearest package.json named jest). */
24
+ function jestPackageRoot(entry) {
25
+ let dir = dirname(entry);
26
+ for (;;) {
27
+ const manifest = join(dir, 'package.json');
28
+ if (existsSync(manifest)) {
29
+ try {
30
+ if (JSON.parse(readFileSync(manifest, 'utf8')).name === 'jest') return dir;
31
+ } catch {
32
+ /* keep walking */
33
+ }
34
+ }
35
+ const parent = resolve(dir, '..');
36
+ if (parent === dir) return null;
37
+ dir = parent;
38
+ }
39
+ }
40
+
41
+ /** `jest/bin/jest.js` as resolved from `cwd` (its `exports` map hides the bin subpath, so derive it). */
42
+ function findJestBin(cwd) {
43
+ try {
44
+ const entry = createRequire(join(resolve(cwd), 'noop.js')).resolve('jest');
45
+ const root = jestPackageRoot(entry);
46
+ if (root) {
47
+ const bin = join(root, 'bin', 'jest.js');
48
+ if (existsSync(bin)) return bin;
49
+ }
50
+ } catch {
51
+ /* fall through to the walk */
52
+ }
53
+ let dir = resolve(cwd);
54
+ for (;;) {
55
+ const bin = join(dir, 'node_modules', 'jest', 'bin', 'jest.js');
56
+ if (existsSync(bin)) return bin;
57
+ const parent = resolve(dir, '..');
58
+ if (parent === dir) return null;
59
+ dir = parent;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Signal handling, as npm and cross-spawn do it:
65
+ * - SIGINT is never forwarded: a Ctrl+C reaches jest from the terminal itself (the console process group
66
+ * on POSIX, CTRL_C_EVENT on Windows), so forwarding would deliver it twice. The launcher only ignores
67
+ * it and waits for jest to finish its own shutdown.
68
+ * - SIGTERM and SIGHUP are forwarded on POSIX (a supervisor signals the launcher, not the group).
69
+ * - On Windows nothing is forwarded: `child.kill(signal)` there is TerminateProcess, which would cut
70
+ * jest's own cleanup short.
71
+ */
72
+ const IGNORED_SIGNALS = ['SIGINT'];
73
+ const FORWARDED_SIGNALS = process.platform === 'win32' ? [] : ['SIGTERM', 'SIGHUP'];
74
+
75
+ function main() {
76
+ const jestBin = findJestBin(process.cwd());
77
+ if (!jestBin) {
78
+ process.stderr.write(
79
+ `mcp-core-jest: could not locate jest from ${process.cwd()}. Add jest as a devDependency of this package and install it.\n`,
80
+ );
81
+ process.exit(1);
82
+ }
83
+ const child = spawn(process.execPath, ['--experimental-vm-modules', jestBin, ...process.argv.slice(2)], {
84
+ stdio: 'inherit',
85
+ shell: false,
86
+ });
87
+ const forwarders = [
88
+ ...IGNORED_SIGNALS.map((signal) => [signal, () => undefined]),
89
+ ...FORWARDED_SIGNALS.map((signal) => [
90
+ signal,
91
+ () => {
92
+ if (child.exitCode === null && child.signalCode === null) child.kill(signal);
93
+ },
94
+ ]),
95
+ ];
96
+ for (const [signal, handler] of forwarders) process.on(signal, handler);
97
+ child.on('error', (err) => {
98
+ process.stderr.write(`mcp-core-jest: could not start jest: ${err.message}\n`);
99
+ process.exit(1);
100
+ });
101
+ child.on('exit', (code, signal) => {
102
+ for (const [name, forward] of forwarders) process.off(name, forward);
103
+ if (signal) {
104
+ process.exitCode = 1;
105
+ process.kill(process.pid, signal);
106
+ return;
107
+ }
108
+ process.exit(code ?? 1);
109
+ });
110
+ }
111
+
112
+ main();
package/dist/auth.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @novorb/mcp-core - HTTP auth
3
+ *
4
+ * mcp-core's HTTP auth runs on the SDK's resource-server gate: `verifyBearerToken` parses the
5
+ * `Authorization` header, calls an `OAuthTokenVerifier`, checks `expectedResource`, `requiredScopes` and
6
+ * the expiry, and `bearerAuthChallengeResponse` maps a failure to the 401 / 403 OAuth error with its
7
+ * `WWW-Authenticate` challenge. The verified `AuthInfo` is set as `req.auth`, which the SDK forwards to
8
+ * the server factory (`RequestContext.authInfo`) and to handlers (`ctx.http.authInfo`), so per-tool
9
+ * `scopeChallenge` / `requireScopes(...)` gates work.
10
+ *
11
+ * The static `MCP_API_KEY` is one verifier (`apiKeyVerifier`): a constant-time comparison (SHA-256 of
12
+ * both sides, `timingSafeEqual`) that answers an `AuthInfo` whose `token` is a placeholder, never the
13
+ * key. A host can pass its own verifier (for example one checking OAuth JWTs) instead.
14
+ */
15
+ import type { ServerResponse } from 'node:http';
16
+ import { type AuthInfo, type OAuthTokenVerifier, type VerifyBearerTokenOptions } from '@modelcontextprotocol/server';
17
+ /** The `AuthInfo.token` the API-key verifier reports: never the real key. */
18
+ export declare const API_KEY_TOKEN_PLACEHOLDER = "[api-key]";
19
+ /** How long an API-key `AuthInfo` is valid (the SDK refuses tokens without `expiresAt`). */
20
+ export declare const API_KEY_TTL_SECONDS = 3600;
21
+ export interface ApiKeyVerifierOptions {
22
+ /** `AuthInfo.clientId` (default `api-key`). */
23
+ clientId?: string;
24
+ /** `AuthInfo.scopes` granted by the key (default none). */
25
+ scopes?: string[];
26
+ /** `AuthInfo.expiresAt` = now + this (default 3600 s). */
27
+ ttlSeconds?: number;
28
+ }
29
+ /** Options for the HTTP auth gate (transport and Express adapter). */
30
+ export interface HttpAuthOptions {
31
+ /** A token verifier (for example a JWT verifier). When set it replaces the static API key. */
32
+ verifier?: OAuthTokenVerifier;
33
+ /** Scopes every request must carry (403 `insufficient_scope` otherwise). */
34
+ requiredScopes?: string[];
35
+ /** RFC 9728 Protected Resource Metadata URL advertised in challenges. */
36
+ resourceMetadataUrl?: string;
37
+ /** Accept only tokens issued for this resource (audience). */
38
+ expectedResource?: URL;
39
+ /** `AuthInfo` fields for the static API key. */
40
+ apiKey?: ApiKeyVerifierOptions;
41
+ }
42
+ /** A verifier for one static key, compared in constant time. The returned `AuthInfo` never holds the key. */
43
+ export declare function apiKeyVerifier(apiKey: string, options?: ApiKeyVerifierOptions): OAuthTokenVerifier;
44
+ /** The SDK bearer options for a gate: the explicit verifier, else the API key, else none (open). */
45
+ export declare function bearerOptions(auth: HttpAuthOptions | undefined, apiKey: string | undefined): VerifyBearerTokenOptions | undefined;
46
+ export type AuthOutcome = {
47
+ ok: true;
48
+ authInfo: AuthInfo;
49
+ } | {
50
+ ok: false;
51
+ response: Response;
52
+ };
53
+ /** Verify an `Authorization` header value; a failure carries the SDK's ready-made challenge response. */
54
+ export declare function authenticate(header: string | undefined, options: VerifyBearerTokenOptions): Promise<AuthOutcome>;
55
+ /** Write a web `Response` (status, headers, body) to a Node response. */
56
+ export declare function writeWebResponse(res: ServerResponse, response: Response): Promise<void>;
57
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,KAAK,QAAQ,EAIb,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAE9B,MAAM,8BAA8B,CAAC;AAEtC,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,MAAM,WAAW,qBAAqB;IACpC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,GAAG,CAAC;IACvB,gDAAgD;IAChD,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAMD,6GAA6G;AAC7G,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,kBAAkB,CAgBtG;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAC3B,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,wBAAwB,GAAG,SAAS,CAStC;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE/F,yGAAyG;AACzG,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,yEAAyE;AACzE,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7F"}
package/dist/auth.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @novorb/mcp-core - HTTP auth
3
+ *
4
+ * mcp-core's HTTP auth runs on the SDK's resource-server gate: `verifyBearerToken` parses the
5
+ * `Authorization` header, calls an `OAuthTokenVerifier`, checks `expectedResource`, `requiredScopes` and
6
+ * the expiry, and `bearerAuthChallengeResponse` maps a failure to the 401 / 403 OAuth error with its
7
+ * `WWW-Authenticate` challenge. The verified `AuthInfo` is set as `req.auth`, which the SDK forwards to
8
+ * the server factory (`RequestContext.authInfo`) and to handlers (`ctx.http.authInfo`), so per-tool
9
+ * `scopeChallenge` / `requireScopes(...)` gates work.
10
+ *
11
+ * The static `MCP_API_KEY` is one verifier (`apiKeyVerifier`): a constant-time comparison (SHA-256 of
12
+ * both sides, `timingSafeEqual`) that answers an `AuthInfo` whose `token` is a placeholder, never the
13
+ * key. A host can pass its own verifier (for example one checking OAuth JWTs) instead.
14
+ */
15
+ import { createHash, timingSafeEqual } from 'node:crypto';
16
+ import { bearerAuthChallengeResponse, OAuthError, OAuthErrorCode, verifyBearerToken, } from '@modelcontextprotocol/server';
17
+ /** The `AuthInfo.token` the API-key verifier reports: never the real key. */
18
+ export const API_KEY_TOKEN_PLACEHOLDER = '[api-key]';
19
+ /** How long an API-key `AuthInfo` is valid (the SDK refuses tokens without `expiresAt`). */
20
+ export const API_KEY_TTL_SECONDS = 3600;
21
+ function digest(value) {
22
+ return createHash('sha256').update(value).digest();
23
+ }
24
+ /** A verifier for one static key, compared in constant time. The returned `AuthInfo` never holds the key. */
25
+ export function apiKeyVerifier(apiKey, options = {}) {
26
+ if (apiKey.length === 0)
27
+ throw new Error('apiKeyVerifier: the API key must not be empty');
28
+ const expected = digest(apiKey);
29
+ return {
30
+ async verifyAccessToken(token) {
31
+ if (!timingSafeEqual(digest(token), expected)) {
32
+ throw new OAuthError(OAuthErrorCode.InvalidToken, 'Invalid token');
33
+ }
34
+ return {
35
+ token: API_KEY_TOKEN_PLACEHOLDER,
36
+ clientId: options.clientId ?? 'api-key',
37
+ scopes: [...(options.scopes ?? [])],
38
+ expiresAt: Math.floor(Date.now() / 1000) + (options.ttlSeconds ?? API_KEY_TTL_SECONDS),
39
+ };
40
+ },
41
+ };
42
+ }
43
+ /** The SDK bearer options for a gate: the explicit verifier, else the API key, else none (open). */
44
+ export function bearerOptions(auth, apiKey) {
45
+ const verifier = auth?.verifier ?? (apiKey ? apiKeyVerifier(apiKey, auth?.apiKey) : undefined);
46
+ if (!verifier)
47
+ return undefined;
48
+ return {
49
+ verifier,
50
+ ...(auth?.requiredScopes ? { requiredScopes: auth.requiredScopes } : {}),
51
+ ...(auth?.resourceMetadataUrl ? { resourceMetadataUrl: auth.resourceMetadataUrl } : {}),
52
+ ...(auth?.expectedResource ? { expectedResource: auth.expectedResource } : {}),
53
+ };
54
+ }
55
+ /** Verify an `Authorization` header value; a failure carries the SDK's ready-made challenge response. */
56
+ export async function authenticate(header, options) {
57
+ try {
58
+ return { ok: true, authInfo: await verifyBearerToken(header, options) };
59
+ }
60
+ catch (error) {
61
+ return { ok: false, response: bearerAuthChallengeResponse(error, options) };
62
+ }
63
+ }
64
+ /** Write a web `Response` (status, headers, body) to a Node response. */
65
+ export async function writeWebResponse(res, response) {
66
+ const headers = {};
67
+ response.headers.forEach((value, key) => {
68
+ headers[key] = value;
69
+ });
70
+ const body = await response.text();
71
+ res.writeHead(response.status, headers);
72
+ res.end(body);
73
+ }
74
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}