@aashari/boilerplate-mcp-server 3.0.0 → 3.2.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.
- package/.releaserc.json +5 -2
- package/CHANGELOG.md +14 -0
- package/README.md +16 -15
- package/SECURITY.md +2 -2
- package/dist/controllers/ipaddress.controller.js +2 -1
- package/dist/index.js +206 -32
- package/dist/tools/ipaddress-link.tool.js +32 -33
- package/dist/tools/ipaddress.tool.js +6 -0
- package/dist/types/common.types.d.ts +5 -0
- package/dist/utils/constants.util.d.ts +1 -1
- package/dist/utils/constants.util.js +1 -1
- package/dist/utils/constants.util.js.bak +1 -1
- package/dist/utils/error-handler.util.js +1 -1
- package/docs/MCP-MODERNIZATION-RELEASE-RUNBOOK.md +306 -0
- package/package.json +23 -33
- package/package.json.bak +23 -33
package/.releaserc.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"branches": [
|
|
2
|
+
"branches": [
|
|
3
|
+
"main"
|
|
4
|
+
],
|
|
3
5
|
"plugins": [
|
|
4
6
|
"@semantic-release/commit-analyzer",
|
|
5
7
|
"@semantic-release/release-notes-generator",
|
|
@@ -24,7 +26,8 @@
|
|
|
24
26
|
"package.json",
|
|
25
27
|
"CHANGELOG.md",
|
|
26
28
|
"src/index.ts",
|
|
27
|
-
"src/cli/index.ts"
|
|
29
|
+
"src/cli/index.ts",
|
|
30
|
+
"src/utils/constants.util.ts"
|
|
28
31
|
],
|
|
29
32
|
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
30
33
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [3.2.0](https://github.com/aashari/boilerplate-mcp-server/compare/v3.1.0...v3.2.0) (2026-02-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* harden HTTP transport security and add tool annotations ([5919f9f](https://github.com/aashari/boilerplate-mcp-server/commit/5919f9f4c1e416da1a6f5c9ce01174a56624ba90))
|
|
7
|
+
|
|
8
|
+
# [3.1.0](https://github.com/aashari/boilerplate-mcp-server/compare/v3.0.0...v3.1.0) (2026-02-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* modernize HTTP transport and standardize MCP tool contracts ([615b373](https://github.com/aashari/boilerplate-mcp-server/commit/615b3732c10ad2c607a899de0f6632221b6f7bc3))
|
|
14
|
+
|
|
1
15
|
# [3.0.0](https://github.com/aashari/boilerplate-mcp-server/compare/v2.0.0...v3.0.0) (2026-02-04)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A production-ready foundation for developing custom Model Context Protocol (MCP)
|
|
|
5
5
|
[](https://www.npmjs.com/package/@aashari/boilerplate-mcp-server)
|
|
6
6
|
[](https://opensource.org/licenses/ISC)
|
|
7
7
|
|
|
8
|
-
> **Latest Update (Feb 2026)**: Updated to MCP SDK 1.
|
|
8
|
+
> **Latest Update (Feb 2026)**: Updated to MCP SDK 1.26.0 and Zod 4.3.6 with continued modern `registerTool` patterns and streamable HTTP improvements. See [docs/MODERNIZATION.md](docs/MODERNIZATION.md) for details.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ A production-ready foundation for developing custom Model Context Protocol (MCP)
|
|
|
18
18
|
- **TOON Output Format**: Token-Oriented Object Notation for 30-60% fewer tokens than JSON
|
|
19
19
|
- **JMESPath Filtering**: Extract only needed fields from responses to reduce token costs
|
|
20
20
|
- **Raw Response Logging**: Automatic logging of large API responses to `/tmp/mcp/<project>/` with truncation guidance
|
|
21
|
-
- **Modern SDK**: Uses MCP SDK v1.
|
|
21
|
+
- **Modern SDK**: Uses MCP SDK v1.26.0 with `registerTool` API pattern (ready for v2 migration)
|
|
22
22
|
- **Complete IP Address Example**: Tools, resources, prompts, and CLI commands for IP geolocation
|
|
23
23
|
- **Comprehensive Testing**: Unit and integration tests with coverage reporting (47 tests passing)
|
|
24
24
|
- **Production Tooling**: ESLint, Prettier, semantic-release, and MCP Inspector integration
|
|
@@ -31,7 +31,7 @@ Model Context Protocol (MCP) is an open standard for securely connecting AI syst
|
|
|
31
31
|
|
|
32
32
|
## Prerequisites
|
|
33
33
|
|
|
34
|
-
- **Node.js** (>=
|
|
34
|
+
- **Node.js** (>=20.x): [Download](https://nodejs.org/)
|
|
35
35
|
- **Git**: For version control
|
|
36
36
|
|
|
37
37
|
## Quick Start
|
|
@@ -263,10 +263,6 @@ npm run mcp:stdio # STDIO transport for AI assistants
|
|
|
263
263
|
npm run mcp:http # HTTP transport on port 3000
|
|
264
264
|
npm run mcp:inspect # HTTP + auto-open MCP Inspector
|
|
265
265
|
|
|
266
|
-
# Development with Debugging
|
|
267
|
-
npm run dev:stdio # STDIO with MCP Inspector integration
|
|
268
|
-
npm run dev:http # HTTP with debug logging enabled
|
|
269
|
-
|
|
270
266
|
# Testing
|
|
271
267
|
npm test # Run all tests (Jest)
|
|
272
268
|
npm run test:coverage # Generate coverage report
|
|
@@ -275,7 +271,6 @@ npm run test:cli # Run CLI-specific tests
|
|
|
275
271
|
# Code Quality
|
|
276
272
|
npm run lint # ESLint with TypeScript rules
|
|
277
273
|
npm run format # Prettier formatting
|
|
278
|
-
npm run update:deps # Update dependencies
|
|
279
274
|
```
|
|
280
275
|
|
|
281
276
|
### Environment Variables
|
|
@@ -665,12 +660,18 @@ npm run cli -- get-ip-details 8.8.8.8 --jq "{ip: query, country: country}" # JM
|
|
|
665
660
|
|
|
666
661
|
**MCP Tools:**
|
|
667
662
|
- `ip_get_details` - IP geolocation lookup for AI assistants
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
663
|
+
- `ip_get_details_link` - Same lookup + resource link output for resource-aware clients
|
|
664
|
+
|
|
665
|
+
Both tools share the same parameters:
|
|
666
|
+
- `ipAddress` (optional): IP address to lookup (omit for current device's public IP)
|
|
667
|
+
- `includeExtendedData` (optional, default: `false`): Include ASN, host, organization data (requires API token)
|
|
668
|
+
- `useHttps` (optional, default: `true`): Use HTTPS for API calls
|
|
669
|
+
- `jq` (optional): JMESPath expression to filter/transform response
|
|
670
|
+
- `outputFormat` (optional, default: `"toon"`): Output format - "toon" or "json"
|
|
671
|
+
|
|
672
|
+
Output behavior:
|
|
673
|
+
- `ip_get_details`: returns one `text` content block
|
|
674
|
+
- `ip_get_details_link`: returns the same first `text` block plus a `resource_link` block (`ip://<resolved-ip>`)
|
|
674
675
|
|
|
675
676
|
**MCP Resources:**
|
|
676
677
|
- `ip://{ipAddress}` - IP details resource template (e.g., `ip://8.8.8.8`, `ip://1.1.1.1`)
|
|
@@ -718,7 +719,7 @@ PORT=3001 # Custom port
|
|
|
718
719
|
npm run cli -- your-command
|
|
719
720
|
npm run mcp:stdio # Test with MCP Inspector
|
|
720
721
|
```
|
|
721
|
-
4. **
|
|
722
|
+
4. **Release:** Push conventional commits to `main` and let semantic-release publish automatically via GitHub Actions (OIDC trusted publishing).
|
|
722
723
|
|
|
723
724
|
## Testing Strategy
|
|
724
725
|
|
package/SECURITY.md
CHANGED
|
@@ -110,7 +110,8 @@ async function get(args = {}) {
|
|
|
110
110
|
const useToon = args.outputFormat !== 'json';
|
|
111
111
|
// Format the output
|
|
112
112
|
const content = await (0, jq_util_js_1.toOutputString)(filteredData, useToon);
|
|
113
|
-
|
|
113
|
+
// Expose canonical resolved IP so callers do not need to parse rendered output
|
|
114
|
+
return { content, rawResponsePath, resolvedIp: data.query };
|
|
114
115
|
}
|
|
115
116
|
catch (error) {
|
|
116
117
|
throw (0, error_handler_util_js_1.handleControllerError)(error, (0, error_handler_util_js_2.buildErrorContext)('IP Address', 'get', 'controllers/ipaddress.controller.ts@get', args.ipAddress || 'current device', { args }));
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.startServer = startServer;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
10
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
10
11
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
12
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
11
13
|
const logger_util_js_1 = require("./utils/logger.util.js");
|
|
12
14
|
const config_util_js_1 = require("./utils/config.util.js");
|
|
13
15
|
const constants_util_js_1 = require("./utils/constants.util.js");
|
|
@@ -22,6 +24,29 @@ const analysis_prompt_js_1 = __importDefault(require("./prompts/analysis.prompt.
|
|
|
22
24
|
const logger = logger_util_js_1.Logger.forContext('index.ts');
|
|
23
25
|
let serverInstance = null;
|
|
24
26
|
let transportInstance = null;
|
|
27
|
+
let httpServerInstance = null;
|
|
28
|
+
const httpSessions = new Map();
|
|
29
|
+
function createMcpServer() {
|
|
30
|
+
const server = new mcp_js_1.McpServer({
|
|
31
|
+
name: constants_util_js_1.PACKAGE_NAME,
|
|
32
|
+
version: constants_util_js_1.VERSION,
|
|
33
|
+
});
|
|
34
|
+
ipaddress_tool_js_1.default.registerTools(server);
|
|
35
|
+
ipaddress_link_tool_js_1.default.registerTools(server);
|
|
36
|
+
ipaddress_resource_js_1.default.registerResources(server);
|
|
37
|
+
analysis_prompt_js_1.default.registerPrompts(server);
|
|
38
|
+
return server;
|
|
39
|
+
}
|
|
40
|
+
function getSessionId(req) {
|
|
41
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
42
|
+
if (Array.isArray(rawSessionId)) {
|
|
43
|
+
return rawSessionId[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
if (typeof rawSessionId === 'string' && rawSessionId.length > 0) {
|
|
46
|
+
return rawSessionId;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
25
50
|
/**
|
|
26
51
|
* Start the MCP server with the specified transport mode
|
|
27
52
|
*/
|
|
@@ -34,19 +59,11 @@ async function startServer(mode = 'stdio') {
|
|
|
34
59
|
if (config_util_js_1.config.getBoolean('DEBUG')) {
|
|
35
60
|
serverLogger.debug('Debug mode enabled');
|
|
36
61
|
}
|
|
37
|
-
serverLogger.info(`Initializing Boilerplate MCP server v${constants_util_js_1.VERSION}`);
|
|
38
|
-
serverInstance = new mcp_js_1.McpServer({
|
|
39
|
-
name: constants_util_js_1.PACKAGE_NAME,
|
|
40
|
-
version: constants_util_js_1.VERSION,
|
|
41
|
-
});
|
|
42
|
-
// Register tools, resources, and prompts
|
|
43
|
-
serverLogger.info('Registering MCP tools, resources, and prompts...');
|
|
44
|
-
ipaddress_tool_js_1.default.registerTools(serverInstance);
|
|
45
|
-
ipaddress_link_tool_js_1.default.registerTools(serverInstance);
|
|
46
|
-
ipaddress_resource_js_1.default.registerResources(serverInstance);
|
|
47
|
-
analysis_prompt_js_1.default.registerPrompts(serverInstance);
|
|
48
|
-
serverLogger.debug('All tools, resources, and prompts registered');
|
|
49
62
|
if (mode === 'stdio') {
|
|
63
|
+
serverLogger.info(`Initializing Boilerplate MCP server v${constants_util_js_1.VERSION}`);
|
|
64
|
+
serverLogger.info('Registering MCP tools, resources, and prompts...');
|
|
65
|
+
serverInstance = createMcpServer();
|
|
66
|
+
serverLogger.debug('All tools, resources, and prompts registered');
|
|
50
67
|
serverLogger.info('Using STDIO transport');
|
|
51
68
|
transportInstance = new stdio_js_1.StdioServerTransport();
|
|
52
69
|
try {
|
|
@@ -76,8 +93,10 @@ async function startServer(mode = 'stdio') {
|
|
|
76
93
|
const allowedOrigins = [
|
|
77
94
|
'http://localhost',
|
|
78
95
|
'http://127.0.0.1',
|
|
96
|
+
'http://[::1]',
|
|
79
97
|
'https://localhost',
|
|
80
98
|
'https://127.0.0.1',
|
|
99
|
+
'https://[::1]',
|
|
81
100
|
];
|
|
82
101
|
const isAllowed = allowedOrigins.some((allowed) => origin === allowed || origin.startsWith(`${allowed}:`));
|
|
83
102
|
if (!isAllowed) {
|
|
@@ -90,30 +109,138 @@ async function startServer(mode = 'stdio') {
|
|
|
90
109
|
}
|
|
91
110
|
next();
|
|
92
111
|
});
|
|
93
|
-
app.use((0, cors_1.default)());
|
|
94
|
-
app.use(express_1.default.json());
|
|
112
|
+
app.use((0, cors_1.default)({ origin: true }));
|
|
113
|
+
app.use(express_1.default.json({ limit: '1mb' }));
|
|
95
114
|
const mcpEndpoint = '/mcp';
|
|
96
115
|
serverLogger.debug(`MCP endpoint: ${mcpEndpoint}`);
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
// Handle MCP POST requests (initialize + normal RPC calls)
|
|
117
|
+
app.post(mcpEndpoint, async (req, res) => {
|
|
118
|
+
const sessionId = getSessionId(req);
|
|
119
|
+
try {
|
|
120
|
+
if (sessionId) {
|
|
121
|
+
const session = httpSessions.get(sessionId);
|
|
122
|
+
if (!session) {
|
|
123
|
+
res.status(404).json({
|
|
124
|
+
jsonrpc: '2.0',
|
|
125
|
+
error: {
|
|
126
|
+
code: -32001,
|
|
127
|
+
message: 'Session not found for provided Mcp-Session-Id',
|
|
128
|
+
},
|
|
129
|
+
id: null,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
session.lastActivity = Date.now();
|
|
134
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!(0, types_js_1.isInitializeRequest)(req.body)) {
|
|
138
|
+
res.status(400).json({
|
|
139
|
+
jsonrpc: '2.0',
|
|
140
|
+
error: {
|
|
141
|
+
code: -32000,
|
|
142
|
+
message: 'Bad Request: Missing Mcp-Session-Id header',
|
|
143
|
+
},
|
|
144
|
+
id: null,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
serverLogger.info('Creating new HTTP MCP session for initialize request');
|
|
149
|
+
let initializedSessionId = null;
|
|
150
|
+
const sessionServer = createMcpServer();
|
|
151
|
+
const sessionTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
152
|
+
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
|
|
153
|
+
onsessioninitialized: (newSessionId) => {
|
|
154
|
+
initializedSessionId = newSessionId;
|
|
155
|
+
httpSessions.set(newSessionId, {
|
|
156
|
+
server: sessionServer,
|
|
157
|
+
transport: sessionTransport,
|
|
158
|
+
lastActivity: Date.now(),
|
|
159
|
+
});
|
|
160
|
+
serverLogger.info(`Initialized HTTP MCP session ${newSessionId}`);
|
|
161
|
+
},
|
|
162
|
+
onsessionclosed: (closedSessionId) => {
|
|
163
|
+
const session = httpSessions.get(closedSessionId);
|
|
164
|
+
if (!session) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
httpSessions.delete(closedSessionId);
|
|
168
|
+
void session.server.close().catch((err) => {
|
|
169
|
+
serverLogger.error(`Error closing server for session ${closedSessionId}`, err);
|
|
170
|
+
});
|
|
171
|
+
serverLogger.info(`Closed HTTP MCP session ${closedSessionId}`);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
await sessionServer.connect(sessionTransport);
|
|
175
|
+
try {
|
|
176
|
+
await sessionTransport.handleRequest(req, res, req.body);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (initializedSessionId) {
|
|
180
|
+
httpSessions.delete(initializedSessionId);
|
|
181
|
+
}
|
|
182
|
+
await sessionTransport.close();
|
|
183
|
+
await sessionServer.close();
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
serverLogger.error('Error in HTTP MCP handler', err);
|
|
111
189
|
if (!res.headersSent) {
|
|
112
190
|
res.status(500).json({
|
|
113
|
-
|
|
191
|
+
jsonrpc: '2.0',
|
|
192
|
+
error: {
|
|
193
|
+
code: -32603,
|
|
194
|
+
message: 'Internal server error',
|
|
195
|
+
},
|
|
196
|
+
id: null,
|
|
114
197
|
});
|
|
115
198
|
}
|
|
116
|
-
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// Handle optional GET requests for streamable HTTP SSE
|
|
202
|
+
app.get(mcpEndpoint, async (req, res) => {
|
|
203
|
+
const sessionId = getSessionId(req);
|
|
204
|
+
if (!sessionId) {
|
|
205
|
+
res.status(400).send('Missing Mcp-Session-Id header');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const session = httpSessions.get(sessionId);
|
|
209
|
+
if (!session) {
|
|
210
|
+
res.status(404).send('Session not found');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await session.transport.handleRequest(req, res);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
serverLogger.error('Error in HTTP MCP GET handler', err);
|
|
218
|
+
if (!res.headersSent) {
|
|
219
|
+
res.status(500).send('Internal Server Error');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// Handle session termination requests
|
|
224
|
+
app.delete(mcpEndpoint, async (req, res) => {
|
|
225
|
+
const sessionId = getSessionId(req);
|
|
226
|
+
if (!sessionId) {
|
|
227
|
+
res.status(400).send('Missing Mcp-Session-Id header');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const session = httpSessions.get(sessionId);
|
|
231
|
+
if (!session) {
|
|
232
|
+
res.status(404).send('Session not found');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await session.transport.handleRequest(req, res);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
serverLogger.error('Error in HTTP MCP DELETE handler', err);
|
|
240
|
+
if (!res.headersSent) {
|
|
241
|
+
res.status(500).send('Internal Server Error');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
117
244
|
});
|
|
118
245
|
// Health check endpoint
|
|
119
246
|
app.get('/', (_req, res) => {
|
|
@@ -123,14 +250,34 @@ async function startServer(mode = 'stdio') {
|
|
|
123
250
|
const PORT = Number(process.env.PORT ?? 3000);
|
|
124
251
|
const HOST = '127.0.0.1'; // Explicit localhost binding for security
|
|
125
252
|
await new Promise((resolve) => {
|
|
126
|
-
app.listen(PORT, HOST, () => {
|
|
253
|
+
httpServerInstance = app.listen(PORT, HOST, () => {
|
|
127
254
|
serverLogger.info(`HTTP transport listening on http://${HOST}:${PORT}${mcpEndpoint}`);
|
|
128
255
|
serverLogger.info('Server bound to localhost only for security');
|
|
129
256
|
resolve();
|
|
130
257
|
});
|
|
131
258
|
});
|
|
259
|
+
// Reap idle sessions every 5 minutes (TTL: 30 minutes)
|
|
260
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
261
|
+
const REAP_INTERVAL_MS = 5 * 60 * 1000;
|
|
262
|
+
const reapInterval = setInterval(() => {
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
for (const [sessionId, session] of httpSessions.entries()) {
|
|
265
|
+
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
266
|
+
serverLogger.info(`Reaping idle HTTP session ${sessionId}`);
|
|
267
|
+
httpSessions.delete(sessionId);
|
|
268
|
+
void session.transport
|
|
269
|
+
.close()
|
|
270
|
+
.catch((err) => serverLogger.debug(`Error closing transport for reaped session ${sessionId}`, err))
|
|
271
|
+
.then(() => session.server.close())
|
|
272
|
+
.catch((err) => serverLogger.debug(`Error closing server for reaped session ${sessionId}`, err));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}, REAP_INTERVAL_MS);
|
|
276
|
+
reapInterval.unref();
|
|
132
277
|
setupGracefulShutdown();
|
|
133
|
-
|
|
278
|
+
// HTTP mode uses per-session servers (managed in httpSessions map).
|
|
279
|
+
// Return a reference server for API compatibility; not connected to any transport.
|
|
280
|
+
return createMcpServer();
|
|
134
281
|
}
|
|
135
282
|
}
|
|
136
283
|
/**
|
|
@@ -174,9 +321,36 @@ if (require.main === module) {
|
|
|
174
321
|
*/
|
|
175
322
|
function setupGracefulShutdown() {
|
|
176
323
|
const shutdownLogger = logger_util_js_1.Logger.forContext('index.ts', 'shutdown');
|
|
324
|
+
let shuttingDown = false;
|
|
177
325
|
const shutdown = async () => {
|
|
326
|
+
if (shuttingDown)
|
|
327
|
+
return;
|
|
328
|
+
shuttingDown = true;
|
|
178
329
|
try {
|
|
179
330
|
shutdownLogger.info('Shutting down gracefully...');
|
|
331
|
+
if (httpSessions.size > 0) {
|
|
332
|
+
shutdownLogger.info(`Closing ${httpSessions.size} active HTTP session(s)`);
|
|
333
|
+
}
|
|
334
|
+
for (const [sessionId, session] of httpSessions.entries()) {
|
|
335
|
+
try {
|
|
336
|
+
await session.transport.close();
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
shutdownLogger.error(`Error closing transport for session ${sessionId}`, err);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
await session.server.close();
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
shutdownLogger.error(`Error closing server for session ${sessionId}`, err);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
httpSessions.clear();
|
|
349
|
+
if (httpServerInstance) {
|
|
350
|
+
await new Promise((resolve) => {
|
|
351
|
+
httpServerInstance.close(() => resolve());
|
|
352
|
+
});
|
|
353
|
+
}
|
|
180
354
|
if (transportInstance &&
|
|
181
355
|
'close' in transportInstance &&
|
|
182
356
|
typeof transportInstance.close === 'function') {
|
|
@@ -5,68 +5,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const logger_util_js_1 = require("../utils/logger.util.js");
|
|
7
7
|
const error_util_js_1 = require("../utils/error.util.js");
|
|
8
|
+
const formatter_util_js_1 = require("../utils/formatter.util.js");
|
|
8
9
|
const zod_1 = require("zod");
|
|
9
10
|
const ipaddress_controller_js_1 = __importDefault(require("../controllers/ipaddress.controller.js"));
|
|
11
|
+
const ipaddress_types_js_1 = require("./ipaddress.types.js");
|
|
10
12
|
const logger = logger_util_js_1.Logger.forContext('tools/ipaddress-link.tool.ts');
|
|
11
13
|
/**
|
|
12
14
|
* Zod schema for the resource-link tool arguments
|
|
15
|
+
* Keep argument shape aligned with ip_get_details for consistency.
|
|
13
16
|
*/
|
|
14
17
|
const GetIpDetailsLinkToolSchema = zod_1.z.object({
|
|
15
18
|
ipAddress: zod_1.z
|
|
16
19
|
.string()
|
|
17
20
|
.optional()
|
|
18
21
|
.describe('IP address to lookup (omit for current IP)'),
|
|
19
|
-
|
|
20
|
-
.boolean()
|
|
21
|
-
.optional()
|
|
22
|
-
.describe('Include extended data (ASN, host, proxy detection)'),
|
|
22
|
+
...ipaddress_types_js_1.IpAddressToolArgs.shape,
|
|
23
23
|
});
|
|
24
24
|
/**
|
|
25
25
|
* Tool description for ip_get_details_link
|
|
26
26
|
*/
|
|
27
|
-
const IP_GET_DETAILS_LINK_DESCRIPTION = `Retrieve IP address details and return
|
|
28
|
-
|
|
29
|
-
**This demonstrates the ResourceLink pattern** - instead of embedding full data inline, the tool returns a reference to a resource. This is useful for:
|
|
30
|
-
- Large responses that would consume many tokens
|
|
31
|
-
- Data that clients may cache and reuse
|
|
32
|
-
- Responses that other tools can reference
|
|
33
|
-
|
|
34
|
-
**When to use ResourceLink vs inline content:**
|
|
35
|
-
- ResourceLink: Large data, cacheable, reusable across multiple tools
|
|
36
|
-
- Inline: Small data, one-time use, immediate context
|
|
27
|
+
const IP_GET_DETAILS_LINK_DESCRIPTION = `Retrieve IP address details and return both direct content and a resource link.
|
|
37
28
|
|
|
38
|
-
**
|
|
39
|
-
-
|
|
40
|
-
-
|
|
29
|
+
**Consistency with ip_get_details:**
|
|
30
|
+
- Uses the same arguments: \`ipAddress\`, \`includeExtendedData\`, \`useHttps\`, \`jq\`, \`outputFormat\`
|
|
31
|
+
- Uses the same output rendering pipeline (TOON by default, JSON when \`outputFormat: "json"\`)
|
|
41
32
|
|
|
42
|
-
**
|
|
33
|
+
**What this returns:**
|
|
34
|
+
- A normal text response (same rendered output as \`ip_get_details\`)
|
|
35
|
+
- A \`resource_link\` entry pointing to \`ip://<resolved-ip>\` for resource-style clients
|
|
43
36
|
|
|
44
37
|
**Note:** Cannot lookup private IPs (192.168.x.x, 10.x.x.x). Powered by ip-api.com.`;
|
|
45
38
|
/**
|
|
46
|
-
* Handle IP lookup with
|
|
39
|
+
* Handle IP lookup with resource-link + text output consistency.
|
|
47
40
|
*/
|
|
48
41
|
async function handleGetIpDetailsLink(args) {
|
|
49
42
|
const methodLogger = logger.forMethod('handleGetIpDetailsLink');
|
|
50
43
|
methodLogger.debug(`Getting IP address details link for ${args.ipAddress || 'current IP'}...`, args);
|
|
51
44
|
try {
|
|
52
|
-
// First, verify we can get the data
|
|
53
45
|
const result = await ipaddress_controller_js_1.default.get(args);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
// Return a ResourceLink instead of inline content
|
|
60
|
-
// This tells the client to fetch the resource separately
|
|
46
|
+
const actualIp = result.resolvedIp ||
|
|
47
|
+
(typeof args.ipAddress === 'string' ? args.ipAddress : 'current');
|
|
48
|
+
methodLogger.debug(`Resolved IP for resource URI: ${actualIp}`);
|
|
49
|
+
const textContent = (0, formatter_util_js_1.truncateForAI)(result.content, result.rawResponsePath);
|
|
50
|
+
const mimeType = args.outputFormat === 'json' ? 'application/json' : 'text/plain';
|
|
61
51
|
return {
|
|
62
52
|
content: [
|
|
63
53
|
{
|
|
64
|
-
type: '
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: textContent,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'resource_link',
|
|
59
|
+
uri: `ip://${actualIp}`,
|
|
60
|
+
name: `IP lookup ${actualIp}`,
|
|
61
|
+
description: `Resource link for IP lookup result ${actualIp}`,
|
|
62
|
+
mimeType,
|
|
70
63
|
},
|
|
71
64
|
],
|
|
72
65
|
};
|
|
@@ -86,6 +79,12 @@ function registerTools(server) {
|
|
|
86
79
|
title: 'IP Address Lookup (ResourceLink)',
|
|
87
80
|
description: IP_GET_DETAILS_LINK_DESCRIPTION,
|
|
88
81
|
inputSchema: GetIpDetailsLinkToolSchema,
|
|
82
|
+
annotations: {
|
|
83
|
+
readOnlyHint: true,
|
|
84
|
+
destructiveHint: false,
|
|
85
|
+
idempotentHint: true,
|
|
86
|
+
openWorldHint: true,
|
|
87
|
+
},
|
|
89
88
|
}, handleGetIpDetailsLink);
|
|
90
89
|
methodLogger.debug('Successfully registered ip_get_details_link tool.');
|
|
91
90
|
}
|
|
@@ -93,6 +93,12 @@ function registerTools(server) {
|
|
|
93
93
|
title: 'IP Address Lookup',
|
|
94
94
|
description: IP_GET_DETAILS_DESCRIPTION,
|
|
95
95
|
inputSchema: GetIpDetailsToolSchema,
|
|
96
|
+
annotations: {
|
|
97
|
+
readOnlyHint: true,
|
|
98
|
+
destructiveHint: false,
|
|
99
|
+
idempotentHint: true,
|
|
100
|
+
openWorldHint: true,
|
|
101
|
+
},
|
|
96
102
|
}, handleGetIpDetails);
|
|
97
103
|
methodLogger.debug('Successfully registered ip_get_details tool.');
|
|
98
104
|
}
|
|
@@ -50,4 +50,9 @@ export interface ControllerResponse {
|
|
|
50
50
|
* When the response is truncated, this path allows AI to access the full data.
|
|
51
51
|
*/
|
|
52
52
|
rawResponsePath?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Canonical resolved IP from the upstream API response.
|
|
55
|
+
* Use this to avoid parsing formatted output when consumers need the queried IP value.
|
|
56
|
+
*/
|
|
57
|
+
resolvedIp?: string;
|
|
53
58
|
}
|
|
@@ -11,7 +11,7 @@ exports.CLI_NAME = exports.PACKAGE_NAME = exports.VERSION = void 0;
|
|
|
11
11
|
* Current application version
|
|
12
12
|
* This should match the version in package.json
|
|
13
13
|
*/
|
|
14
|
-
exports.VERSION = '3.
|
|
14
|
+
exports.VERSION = '3.2.0';
|
|
15
15
|
/**
|
|
16
16
|
* Package name with scope
|
|
17
17
|
* Used for initialization and identification
|
|
@@ -11,7 +11,7 @@ exports.CLI_NAME = exports.PACKAGE_NAME = exports.VERSION = void 0;
|
|
|
11
11
|
* Current application version
|
|
12
12
|
* This should match the version in package.json
|
|
13
13
|
*/
|
|
14
|
-
exports.VERSION = '1.
|
|
14
|
+
exports.VERSION = '3.1.0';
|
|
15
15
|
/**
|
|
16
16
|
* Package name with scope
|
|
17
17
|
* Used for initialization and identification
|
|
@@ -162,7 +162,7 @@ function createUserFriendlyErrorMessage(code, context = {}, originalMessage) {
|
|
|
162
162
|
const entity = entityType
|
|
163
163
|
? `${entityType}${entityIdStr ? ` ${entityIdStr}` : ''}`
|
|
164
164
|
: 'Resource';
|
|
165
|
-
let message
|
|
165
|
+
let message;
|
|
166
166
|
switch (code) {
|
|
167
167
|
case ErrorCode.NOT_FOUND:
|
|
168
168
|
message = `${entity} not found${entityIdStr ? `: ${entityIdStr}` : ''}. Verify the ID is correct and that you have access to this ${entityType?.toLowerCase() || 'resource'}.`;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# MCP Modernization + Release Runbook
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This runbook documents the exact modernization + release approach used in this repository so other MCP servers can follow the same pattern with predictable outcomes.
|
|
6
|
+
|
|
7
|
+
Scope covered:
|
|
8
|
+
|
|
9
|
+
- Baseline audit (history, diffs, dependencies, CI)
|
|
10
|
+
- Streamable HTTP transport hardening
|
|
11
|
+
- Tool input/output contract standardization
|
|
12
|
+
- Inspector-based validation
|
|
13
|
+
- Semantic-release driven publishing
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Case Study Window (This Repo)
|
|
18
|
+
|
|
19
|
+
Reference range analyzed:
|
|
20
|
+
|
|
21
|
+
- From: `v3.0.0` (`fadd464`)
|
|
22
|
+
- To: `HEAD` (`08f8a02`)
|
|
23
|
+
|
|
24
|
+
Key commits:
|
|
25
|
+
|
|
26
|
+
- `615b373` `feat: modernize HTTP transport and standardize MCP tool contracts`
|
|
27
|
+
- `287cb4c` `chore(release): 3.1.0 [skip ci]`
|
|
28
|
+
- `08f8a02` `docs: sync README and security guidance with current tooling`
|
|
29
|
+
|
|
30
|
+
### File-Level Change Summary (`v3.0.0..HEAD`)
|
|
31
|
+
|
|
32
|
+
- `.releaserc.json`
|
|
33
|
+
- `CHANGELOG.md`
|
|
34
|
+
- `README.md`
|
|
35
|
+
- `SECURITY.md`
|
|
36
|
+
- `package-lock.json`
|
|
37
|
+
- `package.json`
|
|
38
|
+
- `src/controllers/ipaddress.controller.ts`
|
|
39
|
+
- `src/index.ts`
|
|
40
|
+
- `src/tools/ipaddress-link.tool.ts`
|
|
41
|
+
- `src/types/common.types.ts`
|
|
42
|
+
- `src/utils/constants.util.ts`
|
|
43
|
+
- `src/utils/error-handler.util.ts`
|
|
44
|
+
|
|
45
|
+
High-impact deltas:
|
|
46
|
+
|
|
47
|
+
- `src/index.ts`: moved HTTP handling to session-aware streamable transport flow.
|
|
48
|
+
- `src/tools/ipaddress-link.tool.ts`: aligned input schema with `ip_get_details`, standardized output to `text` + `resource_link`.
|
|
49
|
+
- `src/controllers/ipaddress.controller.ts` + `src/types/common.types.ts`: exposed canonical `resolvedIp` to avoid parsing rendered output.
|
|
50
|
+
- `package.json` + `package-lock.json`: dependency upgrades and script surface simplification.
|
|
51
|
+
- `.releaserc.json`: added `src/utils/constants.util.ts` to git release assets to prevent version drift.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Phase 1: Baseline Audit
|
|
56
|
+
|
|
57
|
+
### 1.1 Capture release baseline
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git tag --sort=-v:refname | head -n 10
|
|
61
|
+
git log --oneline --decorate vX.Y.Z..HEAD
|
|
62
|
+
git diff --name-status vX.Y.Z..HEAD
|
|
63
|
+
git diff --stat vX.Y.Z..HEAD
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 1.2 Validate release pipeline wiring
|
|
67
|
+
|
|
68
|
+
Check:
|
|
69
|
+
|
|
70
|
+
- `.releaserc.json`
|
|
71
|
+
- `.github/workflows/ci-semantic-release.yml`
|
|
72
|
+
- `package.json` `engines`, scripts, dependencies
|
|
73
|
+
|
|
74
|
+
Release prerequisites:
|
|
75
|
+
|
|
76
|
+
- Conventional commits
|
|
77
|
+
- Semantic-release workflow on push to `main`
|
|
78
|
+
- OIDC trusted publishing (no `NPM_TOKEN` required)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Phase 2: Modernize Streamable HTTP Transport
|
|
83
|
+
|
|
84
|
+
## Problem Pattern
|
|
85
|
+
|
|
86
|
+
Stateless `StreamableHTTPServerTransport` reuse across requests causes failures after `initialize`.
|
|
87
|
+
|
|
88
|
+
## Correct Pattern
|
|
89
|
+
|
|
90
|
+
Use stateful session management:
|
|
91
|
+
|
|
92
|
+
- Create a transport on initialize
|
|
93
|
+
- Generate `Mcp-Session-Id`
|
|
94
|
+
- Store per-session `{ server, transport }`
|
|
95
|
+
- Route POST/GET/DELETE by session
|
|
96
|
+
- Cleanup on session close + process shutdown
|
|
97
|
+
|
|
98
|
+
## Implementation Checklist
|
|
99
|
+
|
|
100
|
+
- Add session map keyed by `Mcp-Session-Id`
|
|
101
|
+
- Validate initialize requests with `isInitializeRequest`
|
|
102
|
+
- Reject non-initialize requests without session header
|
|
103
|
+
- Implement:
|
|
104
|
+
- `POST /mcp` for initialize + rpc
|
|
105
|
+
- `GET /mcp` for SSE stream channel if used
|
|
106
|
+
- `DELETE /mcp` for session termination
|
|
107
|
+
- Add graceful shutdown over all sessions
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Phase 3: Standardize Tool Contracts
|
|
112
|
+
|
|
113
|
+
Target rule for related tools:
|
|
114
|
+
|
|
115
|
+
- Same input schema unless there is a strong functional reason to diverge.
|
|
116
|
+
- Same base output shape for primary payload.
|
|
117
|
+
- Additive behavior only where required (e.g., extra link item).
|
|
118
|
+
|
|
119
|
+
## Applied Pattern
|
|
120
|
+
|
|
121
|
+
For `ip_get_details` and `ip_get_details_link`:
|
|
122
|
+
|
|
123
|
+
- Input parity:
|
|
124
|
+
- `ipAddress`
|
|
125
|
+
- `includeExtendedData`
|
|
126
|
+
- `useHttps`
|
|
127
|
+
- `jq`
|
|
128
|
+
- `outputFormat`
|
|
129
|
+
- Output parity:
|
|
130
|
+
- First content block is always `text` from the same toon/json renderer.
|
|
131
|
+
- Link tool extension:
|
|
132
|
+
- Second block is `resource_link` (`ip://<resolved-ip>`).
|
|
133
|
+
|
|
134
|
+
## Critical anti-pattern to avoid
|
|
135
|
+
|
|
136
|
+
Do not parse rendered text (regex on TOON/JSON output) to recover structured fields.
|
|
137
|
+
|
|
138
|
+
Instead:
|
|
139
|
+
|
|
140
|
+
- expose canonical data from controller/service boundary (`resolvedIp`) and consume that.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Phase 4: Test Matrix (Required)
|
|
145
|
+
|
|
146
|
+
### 4.1 Static quality gates
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm run lint
|
|
150
|
+
npm run build
|
|
151
|
+
npm test -- --runInBand
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4.2 Streamable HTTP protocol flow test (curl)
|
|
155
|
+
|
|
156
|
+
Required sequence:
|
|
157
|
+
|
|
158
|
+
1. `initialize`
|
|
159
|
+
2. `notifications/initialized`
|
|
160
|
+
3. `tools/list`
|
|
161
|
+
4. `tools/call`
|
|
162
|
+
|
|
163
|
+
Assertions:
|
|
164
|
+
|
|
165
|
+
- `initialize` -> `200`, has `mcp-session-id`
|
|
166
|
+
- `notifications/initialized` -> `202`
|
|
167
|
+
- `tools/list` -> `200`
|
|
168
|
+
- `tools/call` -> `200`
|
|
169
|
+
|
|
170
|
+
### 4.3 SDK client compatibility test
|
|
171
|
+
|
|
172
|
+
Use official transport:
|
|
173
|
+
|
|
174
|
+
- `StreamableHTTPClientTransport`
|
|
175
|
+
- `Client` from MCP TS SDK
|
|
176
|
+
|
|
177
|
+
Assertions:
|
|
178
|
+
|
|
179
|
+
- `connect()` succeeds
|
|
180
|
+
- `tools/list` succeeds
|
|
181
|
+
- representative `tools/call` succeeds
|
|
182
|
+
|
|
183
|
+
### 4.4 Inspector validation
|
|
184
|
+
|
|
185
|
+
- CLI check:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx @modelcontextprotocol/inspector --cli "http://127.0.0.1:PORT/mcp" --transport http --method tools/list
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- UI check via proxy token URL.
|
|
192
|
+
|
|
193
|
+
For remote access:
|
|
194
|
+
|
|
195
|
+
- `HOST=0.0.0.0`
|
|
196
|
+
- `ALLOWED_ORIGINS` includes remote UI origin
|
|
197
|
+
- set `MCP_PROXY_FULL_ADDRESS` to reachable host:port
|
|
198
|
+
|
|
199
|
+
Security note: keep auth enabled (`MCP_PROXY_AUTH_TOKEN`), do not use `DANGEROUSLY_OMIT_AUTH`.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Phase 5: Semantic Release Procedure
|
|
204
|
+
|
|
205
|
+
### 5.1 Pre-release checks
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git status --short
|
|
209
|
+
npm run lint && npm run build && npm test -- --runInBand
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 5.2 Commit strategy
|
|
213
|
+
|
|
214
|
+
Use conventional commits:
|
|
215
|
+
|
|
216
|
+
- `fix:` -> patch
|
|
217
|
+
- `feat:` -> minor
|
|
218
|
+
- `feat!:` or `BREAKING CHANGE:` -> major
|
|
219
|
+
|
|
220
|
+
### 5.3 Push and monitor
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git push origin main
|
|
224
|
+
gh run list --repo <owner>/<repo> --limit 5
|
|
225
|
+
gh run watch <run-id> --repo <owner>/<repo>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 5.4 Verify artifacts
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
git fetch --tags origin
|
|
232
|
+
git tag --sort=-v:refname | head
|
|
233
|
+
gh release list --repo <owner>/<repo> --limit 5
|
|
234
|
+
npm view <package-name> version
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Release Config Guardrails
|
|
240
|
+
|
|
241
|
+
### Guardrail 1: Keep version files aligned
|
|
242
|
+
|
|
243
|
+
If custom prepare scripts update files, ensure `@semantic-release/git` assets include them.
|
|
244
|
+
|
|
245
|
+
In this repo:
|
|
246
|
+
|
|
247
|
+
- `scripts/update-version.js` updates `src/utils/constants.util.ts`
|
|
248
|
+
- therefore `src/utils/constants.util.ts` must be in `.releaserc.json` git assets.
|
|
249
|
+
|
|
250
|
+
### Guardrail 2: Avoid script sprawl
|
|
251
|
+
|
|
252
|
+
Keep `package.json` scripts minimal and operationally relevant:
|
|
253
|
+
|
|
254
|
+
- build/lint/test/format
|
|
255
|
+
- core run paths (`cli`, `mcp:stdio`, `mcp:http`, `mcp:inspect`)
|
|
256
|
+
|
|
257
|
+
Remove duplicate aliases that do not add behavior.
|
|
258
|
+
|
|
259
|
+
### Guardrail 3: Update active docs only
|
|
260
|
+
|
|
261
|
+
When behavior changes, update:
|
|
262
|
+
|
|
263
|
+
- `README.md`
|
|
264
|
+
- `SECURITY.md`
|
|
265
|
+
- active setup docs
|
|
266
|
+
|
|
267
|
+
Do not rewrite historical/audit snapshots unless they claim current state.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Reusable Execution Checklist
|
|
272
|
+
|
|
273
|
+
- [ ] Identify release baseline tag and commit window
|
|
274
|
+
- [ ] Review commit history + per-file diff stats
|
|
275
|
+
- [ ] Validate semantic-release + CI workflow config
|
|
276
|
+
- [ ] Modernize streamable HTTP transport to session-safe pattern
|
|
277
|
+
- [ ] Standardize related tool input/output contracts
|
|
278
|
+
- [ ] Remove output parsing hacks (regex/content scraping)
|
|
279
|
+
- [ ] Run lint/build/tests
|
|
280
|
+
- [ ] Run curl protocol flow tests
|
|
281
|
+
- [ ] Run official SDK client tests
|
|
282
|
+
- [ ] Run Inspector CLI + UI tests
|
|
283
|
+
- [ ] Commit with conventional commit type
|
|
284
|
+
- [ ] Push to `main`
|
|
285
|
+
- [ ] Watch semantic-release workflow to completion
|
|
286
|
+
- [ ] Verify git tag, GitHub release, npm version
|
|
287
|
+
- [ ] Sync active docs with final behavior
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Appendix: Commands Used in This Repo
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# History/diff tracing
|
|
295
|
+
git log --oneline --decorate v3.0.0..HEAD
|
|
296
|
+
git diff --name-status v3.0.0..HEAD
|
|
297
|
+
git diff --stat v3.0.0..HEAD
|
|
298
|
+
|
|
299
|
+
# Semantic-release dry check (local repo URL fallback)
|
|
300
|
+
npx semantic-release --dry-run --no-ci --branches main \
|
|
301
|
+
--repository-url file:///absolute/path/to/repo \
|
|
302
|
+
--plugins @semantic-release/commit-analyzer @semantic-release/release-notes-generator
|
|
303
|
+
|
|
304
|
+
# Inspector CLI against streamable HTTP
|
|
305
|
+
npx @modelcontextprotocol/inspector --cli "http://127.0.0.1:3330/mcp" --transport http --method tools/list
|
|
306
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aashari/boilerplate-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,28 +14,18 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"clean": "rm -rf dist coverage",
|
|
17
18
|
"prepare": "npm run build && node scripts/ensure-executable.js",
|
|
18
19
|
"postinstall": "node scripts/ensure-executable.js",
|
|
19
|
-
"
|
|
20
|
+
"lint": "eslint src --ext .ts --config eslint.config.mjs",
|
|
21
|
+
"format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
|
|
20
22
|
"test": "jest",
|
|
21
23
|
"test:coverage": "jest --coverage",
|
|
22
24
|
"test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
|
|
23
|
-
"
|
|
24
|
-
"format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
|
|
25
|
-
"publish:npm": "npm publish",
|
|
26
|
-
"update:check": "npx npm-check-updates",
|
|
27
|
-
"update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
|
|
28
|
-
"update:version": "node scripts/update-version.js",
|
|
25
|
+
"cli": "npm run build && node dist/index.js",
|
|
29
26
|
"mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js",
|
|
30
27
|
"mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js",
|
|
31
|
-
"mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
|
|
32
|
-
"dev:stdio": "npm run build && npx @modelcontextprotocol/inspector -e TRANSPORT_MODE=stdio -e DEBUG=true node dist/index.js",
|
|
33
|
-
"dev:http": "DEBUG=true TRANSPORT_MODE=http npm run build && node dist/index.js",
|
|
34
|
-
"dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
|
|
35
|
-
"dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
|
|
36
|
-
"start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
|
|
37
|
-
"start:cli": "npm run build && node dist/index.js",
|
|
38
|
-
"cli": "npm run build && node dist/index.js"
|
|
28
|
+
"mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
|
|
39
29
|
},
|
|
40
30
|
"keywords": [
|
|
41
31
|
"mcp",
|
|
@@ -56,42 +46,42 @@
|
|
|
56
46
|
"author": "Andi Ashari",
|
|
57
47
|
"license": "ISC",
|
|
58
48
|
"engines": {
|
|
59
|
-
"node": ">=
|
|
49
|
+
"node": ">=20.0.0"
|
|
60
50
|
},
|
|
61
51
|
"devDependencies": {
|
|
62
|
-
"@eslint/js": "^
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
63
53
|
"@semantic-release/changelog": "^6.0.3",
|
|
64
54
|
"@semantic-release/exec": "^7.1.0",
|
|
65
55
|
"@semantic-release/git": "^10.0.1",
|
|
66
|
-
"@semantic-release/github": "^12.0.
|
|
67
|
-
"@semantic-release/npm": "^13.1.
|
|
56
|
+
"@semantic-release/github": "^12.0.6",
|
|
57
|
+
"@semantic-release/npm": "^13.1.4",
|
|
68
58
|
"@types/cors": "^2.8.19",
|
|
69
|
-
"@types/express": "^5.0.
|
|
59
|
+
"@types/express": "^5.0.6",
|
|
70
60
|
"@types/jest": "^30.0.0",
|
|
71
61
|
"@types/jmespath": "^0.15.2",
|
|
72
|
-
"@types/node": "^
|
|
73
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
75
|
-
"eslint": "^
|
|
62
|
+
"@types/node": "^25.2.3",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
64
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
65
|
+
"eslint": "^10.0.0",
|
|
76
66
|
"eslint-config-prettier": "^10.1.8",
|
|
77
67
|
"eslint-plugin-filenames": "^1.3.2",
|
|
78
|
-
"eslint-plugin-prettier": "^5.5.
|
|
68
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
79
69
|
"jest": "^30.2.0",
|
|
80
70
|
"nodemon": "^3.1.11",
|
|
81
|
-
"npm-check-updates": "^19.
|
|
82
|
-
"prettier": "^3.
|
|
83
|
-
"semantic-release": "^25.0.
|
|
84
|
-
"ts-jest": "^29.4.
|
|
71
|
+
"npm-check-updates": "^19.3.2",
|
|
72
|
+
"prettier": "^3.8.1",
|
|
73
|
+
"semantic-release": "^25.0.3",
|
|
74
|
+
"ts-jest": "^29.4.6",
|
|
85
75
|
"ts-node": "^10.9.2",
|
|
86
76
|
"typescript": "^5.9.3",
|
|
87
|
-
"typescript-eslint": "^8.
|
|
77
|
+
"typescript-eslint": "^8.56.0"
|
|
88
78
|
},
|
|
89
79
|
"dependencies": {
|
|
90
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
80
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
91
81
|
"@toon-format/toon": "^2.1.0",
|
|
92
82
|
"commander": "^14.0.3",
|
|
93
83
|
"cors": "^2.8.6",
|
|
94
|
-
"dotenv": "^17.
|
|
84
|
+
"dotenv": "^17.3.1",
|
|
95
85
|
"express": "^5.2.1",
|
|
96
86
|
"jmespath": "^0.16.0",
|
|
97
87
|
"zod": "^4.3.6"
|
package/package.json.bak
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aashari/boilerplate-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,28 +14,18 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"clean": "rm -rf dist coverage",
|
|
17
18
|
"prepare": "npm run build && node scripts/ensure-executable.js",
|
|
18
19
|
"postinstall": "node scripts/ensure-executable.js",
|
|
19
|
-
"
|
|
20
|
+
"lint": "eslint src --ext .ts --config eslint.config.mjs",
|
|
21
|
+
"format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
|
|
20
22
|
"test": "jest",
|
|
21
23
|
"test:coverage": "jest --coverage",
|
|
22
24
|
"test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
|
|
23
|
-
"
|
|
24
|
-
"format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
|
|
25
|
-
"publish:npm": "npm publish",
|
|
26
|
-
"update:check": "npx npm-check-updates",
|
|
27
|
-
"update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
|
|
28
|
-
"update:version": "node scripts/update-version.js",
|
|
25
|
+
"cli": "npm run build && node dist/index.js",
|
|
29
26
|
"mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js",
|
|
30
27
|
"mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js",
|
|
31
|
-
"mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
|
|
32
|
-
"dev:stdio": "npm run build && npx @modelcontextprotocol/inspector -e TRANSPORT_MODE=stdio -e DEBUG=true node dist/index.js",
|
|
33
|
-
"dev:http": "DEBUG=true TRANSPORT_MODE=http npm run build && node dist/index.js",
|
|
34
|
-
"dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
|
|
35
|
-
"dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
|
|
36
|
-
"start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
|
|
37
|
-
"start:cli": "npm run build && node dist/index.js",
|
|
38
|
-
"cli": "npm run build && node dist/index.js"
|
|
28
|
+
"mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
|
|
39
29
|
},
|
|
40
30
|
"keywords": [
|
|
41
31
|
"mcp",
|
|
@@ -56,42 +46,42 @@
|
|
|
56
46
|
"author": "Andi Ashari",
|
|
57
47
|
"license": "ISC",
|
|
58
48
|
"engines": {
|
|
59
|
-
"node": ">=
|
|
49
|
+
"node": ">=20.0.0"
|
|
60
50
|
},
|
|
61
51
|
"devDependencies": {
|
|
62
|
-
"@eslint/js": "^
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
63
53
|
"@semantic-release/changelog": "^6.0.3",
|
|
64
54
|
"@semantic-release/exec": "^7.1.0",
|
|
65
55
|
"@semantic-release/git": "^10.0.1",
|
|
66
|
-
"@semantic-release/github": "^12.0.
|
|
67
|
-
"@semantic-release/npm": "^13.1.
|
|
56
|
+
"@semantic-release/github": "^12.0.6",
|
|
57
|
+
"@semantic-release/npm": "^13.1.4",
|
|
68
58
|
"@types/cors": "^2.8.19",
|
|
69
|
-
"@types/express": "^5.0.
|
|
59
|
+
"@types/express": "^5.0.6",
|
|
70
60
|
"@types/jest": "^30.0.0",
|
|
71
61
|
"@types/jmespath": "^0.15.2",
|
|
72
|
-
"@types/node": "^
|
|
73
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
75
|
-
"eslint": "^
|
|
62
|
+
"@types/node": "^25.2.3",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
64
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
65
|
+
"eslint": "^10.0.0",
|
|
76
66
|
"eslint-config-prettier": "^10.1.8",
|
|
77
67
|
"eslint-plugin-filenames": "^1.3.2",
|
|
78
|
-
"eslint-plugin-prettier": "^5.5.
|
|
68
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
79
69
|
"jest": "^30.2.0",
|
|
80
70
|
"nodemon": "^3.1.11",
|
|
81
|
-
"npm-check-updates": "^19.
|
|
82
|
-
"prettier": "^3.
|
|
83
|
-
"semantic-release": "^25.0.
|
|
84
|
-
"ts-jest": "^29.4.
|
|
71
|
+
"npm-check-updates": "^19.3.2",
|
|
72
|
+
"prettier": "^3.8.1",
|
|
73
|
+
"semantic-release": "^25.0.3",
|
|
74
|
+
"ts-jest": "^29.4.6",
|
|
85
75
|
"ts-node": "^10.9.2",
|
|
86
76
|
"typescript": "^5.9.3",
|
|
87
|
-
"typescript-eslint": "^8.
|
|
77
|
+
"typescript-eslint": "^8.56.0"
|
|
88
78
|
},
|
|
89
79
|
"dependencies": {
|
|
90
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
80
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
91
81
|
"@toon-format/toon": "^2.1.0",
|
|
92
82
|
"commander": "^14.0.3",
|
|
93
83
|
"cors": "^2.8.6",
|
|
94
|
-
"dotenv": "^17.
|
|
84
|
+
"dotenv": "^17.3.1",
|
|
95
85
|
"express": "^5.2.1",
|
|
96
86
|
"jmespath": "^0.16.0",
|
|
97
87
|
"zod": "^4.3.6"
|