@novorb/mcp-core 2.1.1 → 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 +82 -161
  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 +278 -43
  44. package/dist/http-client.d.ts.map +1 -1
  45. package/dist/http-client.js +620 -182
  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 +49 -10
  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 -36
  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 +144 -53
  192. package/dist/transport.d.ts.map +1 -1
  193. package/dist/transport.js +305 -122
  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 +140 -18
  237. package/dist/logger.d.ts +0 -80
  238. package/dist/logger.d.ts.map +0 -1
  239. package/dist/logger.js +0 -236
  240. package/dist/logger.js.map +0 -1
package/README.md CHANGED
@@ -4,195 +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-122-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
- 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.
11
+ #### Quality Assurance
12
12
 
13
- ## Features
14
-
15
- - **Structured logging** — file output with rotation, stderr output, text or JSON format, secret redaction
16
- - **HTTP client** — axios wrapper with automatic retry on 429/5xx, request logging, pagination
17
- - **Retry with backoff** — exponential backoff with jitter, configurable retries and delays
18
- - **Pagination helpers** — normalize params, compute metadata, paginate arrays in memory
19
- - **Unified transport** — stdio and stateless streamable-HTTP with built-in auth and DNS-rebinding protection
20
- - **Server builder** — `McpServer` factory, annotation presets, and SDK re-exports from one import point
21
- - **ESM native** — built as ES modules with full TypeScript declarations
22
-
23
- ## Documentation
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
+ [![Fallow](https://img.shields.io/badge/fallow_health-86.6_(A)-brightgreen)](https://fallow.tools)
16
+ [![Maintainability](https://img.shields.io/badge/maintainability-94-brightgreen)](https://fallow.tools)
24
17
 
25
- - **[CLAUDE.md](CLAUDE.md)** — agent/contributor context and working conventions
26
- - **[docs/TESTING.md](docs/TESTING.md)** — test runner commands, conventions, and guidelines
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.
27
19
 
28
- ## Installation
29
-
30
- ```bash
31
- pnpm add @novorb/mcp-core
32
- ```
20
+ ## Features
33
21
 
34
- ## Modules
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
35
35
 
36
- ### Logger
36
+ ## Documentation
37
37
 
38
- 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.
38
+ **Getting started**
39
39
 
40
- ```typescript
41
- import { initLogger, LogLevel } from '@novorb/mcp-core';
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
42
45
 
43
- // File-based (writes to logs/mcp.log with rotation)
44
- const logger = initLogger({
45
- output: 'file',
46
- logDir: './logs',
47
- level: LogLevel.INFO,
48
- });
46
+ **Serving**
49
47
 
50
- // Stderr with structured JSON
51
- const logger = initLogger({
52
- output: 'stderr',
53
- format: 'json',
54
- });
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)
55
53
 
56
- logger.info('Server started', 'Server');
57
- logger.error('Request failed', 'HttpClient', { status: 500, path: '/api/items' });
58
- ```
54
+ **Upstreams and configuration**
59
55
 
60
- ### HTTP Client
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)
61
63
 
62
- 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.
64
+ **Testing and contributing**
63
65
 
64
- ```typescript
65
- import { createHttpClient } from '@novorb/mcp-core';
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
66
69
 
67
- const client = createHttpClient({
68
- baseURL: 'https://api.example.com',
69
- headers: { 'Authorization': 'Bearer token' },
70
- retries: 3,
71
- logContext: 'MyApiClient',
72
- });
70
+ ## Quick Start
73
71
 
74
- const item = await client.get('/items/1');
75
- const { data, totalCount } = await client.getPaginated('/items', { page: 1, per_page: 50 });
76
- await client.post('/items', { name: 'new item' });
72
+ ```bash
73
+ pnpm add @novorb/mcp-core zod
77
74
  ```
78
75
 
79
- ### Retry
80
-
81
- Exponential backoff with jitter for retrying failed operations. Used internally by the HTTP client, but also available standalone.
82
-
83
76
  ```typescript
84
- import { retryWithBackoff, isRetryableError } from '@novorb/mcp-core';
85
-
86
- const result = await retryWithBackoff(async (ctx) => {
87
- console.log(`Attempt ${ctx.attempt + 1}/${ctx.maxRetries + 1}`);
88
- return await someUnreliableOperation();
89
- }, {
90
- maxRetries: 3,
91
- initialDelayMs: 1000,
92
- 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
+ },
93
103
  });
94
104
  ```
95
105
 
96
- ### Pagination
97
-
98
- Normalize pagination parameters, compute metadata, and paginate arrays in memory.
99
-
100
- ```typescript
101
- import { normalizePagination, createPaginationMetadata, paginateArray } from '@novorb/mcp-core';
102
-
103
- const params = normalizePagination({ page: 3, per_page: 200 });
104
- // { page: 3, per_page: 100 } (capped at max)
105
-
106
- const metadata = createPaginationMetadata(500, params);
107
- // { totalCount: 500, page: 3, perPage: 100, totalPages: 5 }
108
-
109
- const { data, metadata } = paginateArray(allItems, { page: 2, per_page: 20 });
110
- ```
111
-
112
- ### Transport
113
-
114
- 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.
115
-
116
- ```typescript
117
- import { startMcpServer } from '@novorb/mcp-core/transport';
118
-
119
- // Factory (recommended for HTTP — fresh server instance per request):
120
- await startMcpServer(() => createServer(services));
121
-
122
- // Server instance (fine for stdio):
123
- await startMcpServer(server);
124
- ```
125
-
126
- | Variable | Default | Description |
127
- |----------|---------|-------------|
128
- | `MCP_TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
129
- | `PORT` | `8080` | HTTP listen port |
130
- | `MCP_HTTP_HOST` | `0.0.0.0` | Bind address (`127.0.0.1` for local-only) |
131
- | `MCP_API_KEY` | none | Bearer token for HTTP auth (required in HTTP mode unless disabled) |
132
- | `MCP_AUTH_DISABLED` | `false` | Set `true` to run HTTP without auth (only behind a fronting gateway) |
133
- | `MCP_MAX_BODY_BYTES` | 4 MiB | Max accepted POST body size |
134
- | `MCP_ALLOWED_HOSTS` | none | Comma-separated Host allowlist (enables DNS-rebinding protection) |
135
- | `MCP_ALLOWED_ORIGINS` | none | Comma-separated Origin allowlist (enables DNS-rebinding protection) |
136
-
137
- ### Server
138
-
139
- Import MCP server-building primitives from here instead of the SDK directly, to centralise the MCP dependency.
140
-
141
- ```typescript
142
- import { createMcpServer, TOOL_ANNOTATIONS, completable } from '@novorb/mcp-core/server';
143
- import type { McpToolAnnotations, McpCallToolResult } from '@novorb/mcp-core/server';
144
-
145
- const server = createMcpServer('my-mcp', '1.0.0');
146
-
147
- server.registerTool('lookup', {
148
- description: 'Look up a value',
149
- inputSchema: { key: z.string() },
150
- outputSchema: { value: z.string(), found: z.boolean() },
151
- annotations: TOOL_ANNOTATIONS.readOnly,
152
- }, async ({ key }) => ({
153
- content: [{ type: 'text', text: value }],
154
- structuredContent: { value, found: true },
155
- }));
156
- ```
157
-
158
- **`TOOL_ANNOTATIONS` presets:** `readOnly` (lookups/searches), `idempotentWrite` (upserts), `mutableWrite` (non-idempotent writes), `destructive` (deletes/overwrites).
159
-
160
- ## Testing
161
-
162
- ```bash
163
- pnpm test # 122 tests
164
- pnpm run test:watch # watch mode
165
- pnpm run test:coverage
166
- ```
167
-
168
- > **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).
169
-
170
- ## Building
171
-
172
- ```bash
173
- pnpm run build # tsc → dist/
174
- ```
175
-
176
- This is an ESM package (`"type": "module"`). `tsconfig.json` is self-contained so it builds standalone.
177
-
178
- ## CI / Releasing
179
-
180
- GitHub Actions (`.github/workflows/`):
181
-
182
- - **CI** (`ci.yml`) — runs build + test on every push to `main` and on pull requests.
183
- - **Publish** (`publish.yml`) — on a pushed version tag `v*`, runs build + test, verifies the
184
- 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).
185
107
 
186
- Release flow:
108
+ ## Development
187
109
 
188
110
  ```bash
189
- # 1. bump "version" in package.json (e.g. 2.1.1), commit
190
- # 2. tag and push — the tag triggers the publish workflow
191
- 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
192
114
  ```
193
115
 
194
- Requires the repo secret **`NPM_TOKEN`** (an npm automation/granular token with publish rights for
195
- the `@novorb` scope) under *Settings → Secrets and variables → Actions*.
116
+ This is an ESM package (`"type": "module"`). See [Testing mcp-core](docs/testing.md).
196
117
 
197
118
  ## License
198
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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"}