@prmichaelsen/task-mcp 0.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/.env.example +19 -0
- package/AGENT.md +1165 -0
- package/CHANGELOG.md +72 -0
- package/agent/commands/acp.commit.md +511 -0
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.package-install.md +347 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.report.md +392 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +323 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +385 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/commands/git.commit.md +511 -0
- package/agent/commands/git.init.md +513 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/acp-task-execution-requirements.md +555 -0
- package/agent/design/api-dto-design.md +394 -0
- package/agent/design/code-extraction-guide.md +827 -0
- package/agent/design/design.template.md +136 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/design/rest-api-integration.md +489 -0
- package/agent/design/sdk-export-requirements.md +549 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-task-infrastructure.md +232 -0
- package/agent/milestones/milestone-4-autonomous-execution.md +235 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.md +1271 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +364 -0
- package/agent/progress.template.yaml +158 -0
- package/agent/progress.yaml +375 -0
- package/agent/scripts/check-for-updates.sh +88 -0
- package/agent/scripts/install.sh +157 -0
- package/agent/scripts/uninstall.sh +75 -0
- package/agent/scripts/update.sh +139 -0
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-{title}.template.md +225 -0
- package/agent/tasks/task-86-task-data-model-schemas.md +143 -0
- package/agent/tasks/task-87-task-database-service.md +220 -0
- package/agent/tasks/task-88-firebase-client-wrapper.md +139 -0
- package/agent/tasks/task-88-task-execution-engine.md +277 -0
- package/agent/tasks/task-89-mcp-server-implementation.md +197 -0
- package/agent/tasks/task-90-build-configuration.md +146 -0
- package/agent/tasks/task-91-deployment-configuration.md +128 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/client.ts.html +1030 -0
- package/coverage/lcov-report/src/constant/collections.ts.html +469 -0
- package/coverage/lcov-report/src/constant/index.html +116 -0
- package/coverage/lcov-report/src/dto/index.html +116 -0
- package/coverage/lcov-report/src/dto/transformers.ts.html +568 -0
- package/coverage/lcov-report/src/index.html +146 -0
- package/coverage/lcov-report/src/schemas/index.html +116 -0
- package/coverage/lcov-report/src/schemas/task.ts.html +547 -0
- package/coverage/lcov-report/src/server-factory.ts.html +418 -0
- package/coverage/lcov-report/src/server.ts.html +289 -0
- package/coverage/lcov-report/src/services/index.html +116 -0
- package/coverage/lcov-report/src/services/task-database.service.ts.html +1495 -0
- package/coverage/lcov-report/src/tools/index.html +236 -0
- package/coverage/lcov-report/src/tools/index.ts.html +292 -0
- package/coverage/lcov-report/src/tools/task-add-message.ts.html +277 -0
- package/coverage/lcov-report/src/tools/task-complete-task-item.ts.html +343 -0
- package/coverage/lcov-report/src/tools/task-create-milestone.ts.html +286 -0
- package/coverage/lcov-report/src/tools/task-create-task-item.ts.html +358 -0
- package/coverage/lcov-report/src/tools/task-get-next-step.ts.html +460 -0
- package/coverage/lcov-report/src/tools/task-get-status.ts.html +316 -0
- package/coverage/lcov-report/src/tools/task-report-completion.ts.html +343 -0
- package/coverage/lcov-report/src/tools/task-update-progress.ts.html +232 -0
- package/coverage/lcov.info +974 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/client.ts.html +1030 -0
- package/coverage/src/constant/collections.ts.html +469 -0
- package/coverage/src/constant/index.html +116 -0
- package/coverage/src/dto/index.html +116 -0
- package/coverage/src/dto/transformers.ts.html +568 -0
- package/coverage/src/index.html +146 -0
- package/coverage/src/schemas/index.html +116 -0
- package/coverage/src/schemas/task.ts.html +547 -0
- package/coverage/src/server-factory.ts.html +418 -0
- package/coverage/src/server.ts.html +289 -0
- package/coverage/src/services/index.html +116 -0
- package/coverage/src/services/task-database.service.ts.html +1495 -0
- package/coverage/src/tools/index.html +236 -0
- package/coverage/src/tools/index.ts.html +292 -0
- package/coverage/src/tools/task-add-message.ts.html +277 -0
- package/coverage/src/tools/task-complete-task-item.ts.html +343 -0
- package/coverage/src/tools/task-create-milestone.ts.html +286 -0
- package/coverage/src/tools/task-create-task-item.ts.html +358 -0
- package/coverage/src/tools/task-get-next-step.ts.html +460 -0
- package/coverage/src/tools/task-get-status.ts.html +316 -0
- package/coverage/src/tools/task-report-completion.ts.html +343 -0
- package/coverage/src/tools/task-update-progress.ts.html +232 -0
- package/firestore.rules +95 -0
- package/jest.config.js +31 -0
- package/package.json +67 -0
- package/src/client.spec.ts +199 -0
- package/src/client.ts +315 -0
- package/src/constant/collections.ts +128 -0
- package/src/dto/index.ts +47 -0
- package/src/dto/task-api.dto.ts +219 -0
- package/src/dto/transformers.spec.ts +462 -0
- package/src/dto/transformers.ts +161 -0
- package/src/schemas/task.ts +154 -0
- package/src/server-factory.spec.ts +70 -0
- package/src/server-factory.ts +111 -0
- package/src/server.ts +68 -0
- package/src/services/task-database.service.e2e.ts +116 -0
- package/src/services/task-database.service.spec.ts +479 -0
- package/src/services/task-database.service.ts +470 -0
- package/src/test-schemas.ts +161 -0
- package/src/tools/index.ts +69 -0
- package/src/tools/task-add-message.ts +64 -0
- package/src/tools/task-complete-task-item.ts +86 -0
- package/src/tools/task-create-milestone.ts +67 -0
- package/src/tools/task-create-task-item.ts +91 -0
- package/src/tools/task-get-next-step.spec.ts +136 -0
- package/src/tools/task-get-next-step.ts +125 -0
- package/src/tools/task-get-status.spec.ts +213 -0
- package/src/tools/task-get-status.ts +77 -0
- package/src/tools/task-report-completion.ts +86 -0
- package/src/tools/task-update-progress.ts +49 -0
- package/src/tools/tools.spec.ts +194 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
# MCP Server Bootstrap Pattern
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document describes the organizational patterns for bootstrapping a new MCP (Model Context Protocol) server library. The focus is on **structure and organization** rather than specific tools or technologies. This pattern is compatible with [`mcp-auth`](https://github.com/prmichaelsen/mcp-auth) multi-tenant authentication patterns.
|
|
6
|
+
|
|
7
|
+
## Core Principles
|
|
8
|
+
|
|
9
|
+
1. **Separation of Concerns**: Clear boundaries between server logic, business logic, and infrastructure
|
|
10
|
+
2. **Multi-Tenant Ready**: Architecture supports per-user/per-tenant server instances
|
|
11
|
+
3. **Type Safety**: Strong typing throughout with TypeScript
|
|
12
|
+
4. **Modular Tools**: Each tool is self-contained with definition and handler
|
|
13
|
+
5. **Dual Build Strategy**: Bundle for CLI, preserve modules for library usage
|
|
14
|
+
6. **ESM-First**: Modern ES modules with proper `.js` extensions in imports
|
|
15
|
+
|
|
16
|
+
## Project Structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
project-root/
|
|
20
|
+
├── src/
|
|
21
|
+
│ ├── index.ts # CLI entry point (bundled)
|
|
22
|
+
│ ├── server.ts # Server class (for standalone)
|
|
23
|
+
│ ├── server-factory.ts # Factory function (for multi-tenant)
|
|
24
|
+
│ ├── client.ts # External API client wrapper
|
|
25
|
+
│ ├── types.ts # Shared type definitions
|
|
26
|
+
│ │
|
|
27
|
+
│ ├── tools/ # Tool definitions
|
|
28
|
+
│ │ ├── index.ts # Tool exports
|
|
29
|
+
│ │ ├── tool-one.ts # Individual tool (definition + handler)
|
|
30
|
+
│ │ ├── tool-two.ts
|
|
31
|
+
│ │ └── ...
|
|
32
|
+
│ │
|
|
33
|
+
│ ├── types/ # Type definitions (optional subdirectory)
|
|
34
|
+
│ │ ├── mcp.ts # MCP-specific types
|
|
35
|
+
│ │ ├── api.ts # External API types
|
|
36
|
+
│ │ └── ...
|
|
37
|
+
│ │
|
|
38
|
+
│ └── utils/ # Utilities
|
|
39
|
+
│ ├── logger.ts # Logging (stdio-safe for MCP)
|
|
40
|
+
│ ├── error-serializer.ts # Error handling
|
|
41
|
+
│ └── ...
|
|
42
|
+
│
|
|
43
|
+
├── agent/ # Documentation & planning
|
|
44
|
+
│ ├── patterns/ # Architecture patterns
|
|
45
|
+
│ ├── tasks/ # Task tracking
|
|
46
|
+
│ └── ...
|
|
47
|
+
│
|
|
48
|
+
├── package.json # Package configuration
|
|
49
|
+
├── tsconfig.json # TypeScript configuration
|
|
50
|
+
├── esbuild.build.js # Build script
|
|
51
|
+
├── esbuild.watch.js # Watch mode script
|
|
52
|
+
├── .gitignore
|
|
53
|
+
└── README.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration Files
|
|
57
|
+
|
|
58
|
+
### package.json Structure
|
|
59
|
+
|
|
60
|
+
For a simple MCP server:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"name": "remember-mcp",
|
|
65
|
+
"version": "0.1.0",
|
|
66
|
+
"description": "Multi-tenant memory system MCP server with vector search and relationships",
|
|
67
|
+
"main": "dist/server.js",
|
|
68
|
+
"type": "module",
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/prmichaelsen/remember-mcp.git"
|
|
72
|
+
},
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/prmichaelsen/remember-mcp/issues"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://github.com/prmichaelsen/remember-mcp#readme",
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "node esbuild.build.js",
|
|
79
|
+
"build:watch": "node esbuild.watch.js",
|
|
80
|
+
"clean": "rm -rf dist",
|
|
81
|
+
"dev": "tsx watch src/server.ts",
|
|
82
|
+
"start": "node dist/server.js",
|
|
83
|
+
"test": "jest",
|
|
84
|
+
"test:watch": "jest --watch",
|
|
85
|
+
"test:e2e": "jest --config jest.e2e.config.js",
|
|
86
|
+
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
|
|
87
|
+
"test:all": "npm test && npm run test:e2e",
|
|
88
|
+
"lint": "eslint src/**/*.ts",
|
|
89
|
+
"typecheck": "tsc --noEmit",
|
|
90
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
91
|
+
},
|
|
92
|
+
"keywords": [
|
|
93
|
+
"mcp",
|
|
94
|
+
"memory",
|
|
95
|
+
"vector-search",
|
|
96
|
+
"weaviate",
|
|
97
|
+
"firebase"
|
|
98
|
+
],
|
|
99
|
+
"author": "Patrick Michaelsen",
|
|
100
|
+
"license": "MIT"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For a library with multiple exports:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"name": "@scope/package-name",
|
|
109
|
+
"version": "1.0.0",
|
|
110
|
+
"description": "MCP server for [purpose]",
|
|
111
|
+
"type": "module",
|
|
112
|
+
"main": "dist/index.js",
|
|
113
|
+
"types": "dist/index.d.ts",
|
|
114
|
+
|
|
115
|
+
"repository": {
|
|
116
|
+
"type": "git",
|
|
117
|
+
"url": "git+https://github.com/username/repo.git"
|
|
118
|
+
},
|
|
119
|
+
"bugs": {
|
|
120
|
+
"url": "https://github.com/username/repo/issues"
|
|
121
|
+
},
|
|
122
|
+
"homepage": "https://github.com/username/repo#readme",
|
|
123
|
+
|
|
124
|
+
"exports": {
|
|
125
|
+
".": {
|
|
126
|
+
"types": "./dist/index.d.ts",
|
|
127
|
+
"import": "./dist/index.js"
|
|
128
|
+
},
|
|
129
|
+
"./factory": {
|
|
130
|
+
"types": "./dist/server-factory.d.ts",
|
|
131
|
+
"import": "./dist/server-factory.js"
|
|
132
|
+
},
|
|
133
|
+
"./client": {
|
|
134
|
+
"types": "./dist/client.d.ts",
|
|
135
|
+
"import": "./dist/client.js"
|
|
136
|
+
},
|
|
137
|
+
"./tools": {
|
|
138
|
+
"types": "./dist/tools/index.d.ts",
|
|
139
|
+
"import": "./dist/tools/index.js"
|
|
140
|
+
},
|
|
141
|
+
"./types": {
|
|
142
|
+
"types": "./dist/types.d.ts",
|
|
143
|
+
"import": "./dist/types.js"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
"files": [
|
|
148
|
+
"dist",
|
|
149
|
+
"README.md",
|
|
150
|
+
"LICENSE"
|
|
151
|
+
],
|
|
152
|
+
|
|
153
|
+
"scripts": {
|
|
154
|
+
"build": "npm run build:types && npm run build:bundle",
|
|
155
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
156
|
+
"build:bundle": "node esbuild.build.js",
|
|
157
|
+
"build:watch": "node esbuild.watch.js",
|
|
158
|
+
"start": "node dist/index.js",
|
|
159
|
+
"clean": "rm -rf dist",
|
|
160
|
+
"test": "jest",
|
|
161
|
+
"test:watch": "jest --watch",
|
|
162
|
+
"test:e2e": "jest --config jest.e2e.config.js",
|
|
163
|
+
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
|
|
164
|
+
"test:all": "npm test && npm run test:e2e",
|
|
165
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
"keywords": [
|
|
169
|
+
"mcp",
|
|
170
|
+
"model-context-protocol",
|
|
171
|
+
"[domain-specific-keywords]"
|
|
172
|
+
],
|
|
173
|
+
|
|
174
|
+
"dependencies": {
|
|
175
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
"devDependencies": {
|
|
179
|
+
"@types/jest": "^30.0.0",
|
|
180
|
+
"@types/node": "^20.0.0",
|
|
181
|
+
"esbuild": "^0.25.0",
|
|
182
|
+
"jest": "^30.0.0",
|
|
183
|
+
"ts-jest": "^29.0.0",
|
|
184
|
+
"typescript": "^5.3.0"
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
"engines": {
|
|
188
|
+
"node": ">=18.0.0"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Key Points:**
|
|
194
|
+
- `"type": "module"` required for ESM
|
|
195
|
+
- `repository`, `bugs`, `homepage` for GitHub integration
|
|
196
|
+
- `main` points to the built entry file
|
|
197
|
+
- `exports` field for libraries with multiple entry points
|
|
198
|
+
- `files` array specifies what to publish to npm
|
|
199
|
+
- `build:watch` script for development
|
|
200
|
+
- `clean` script removes build artifacts
|
|
201
|
+
- `prepublishOnly` ensures clean build before publishing
|
|
202
|
+
- `test` scripts for jest (unit and e2e)
|
|
203
|
+
- `author` field for attribution
|
|
204
|
+
- `engines` specifies minimum Node.js version
|
|
205
|
+
|
|
206
|
+
### tsconfig.json Structure
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"compilerOptions": {
|
|
211
|
+
"target": "ES2022",
|
|
212
|
+
"module": "Node16",
|
|
213
|
+
"moduleResolution": "Node16",
|
|
214
|
+
"lib": ["ES2022"],
|
|
215
|
+
"types": ["node"],
|
|
216
|
+
|
|
217
|
+
"outDir": "./dist",
|
|
218
|
+
"rootDir": "./src",
|
|
219
|
+
|
|
220
|
+
"strict": true,
|
|
221
|
+
"esModuleInterop": true,
|
|
222
|
+
"skipLibCheck": true,
|
|
223
|
+
"forceConsistentCasingInFileNames": true,
|
|
224
|
+
"resolveJsonModule": true,
|
|
225
|
+
|
|
226
|
+
"declaration": true,
|
|
227
|
+
"declarationMap": true,
|
|
228
|
+
"sourceMap": true,
|
|
229
|
+
|
|
230
|
+
"baseUrl": ".",
|
|
231
|
+
"paths": {
|
|
232
|
+
"@/*": ["src/*"]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
"include": ["src/**/*"],
|
|
237
|
+
"exclude": ["node_modules", "dist"]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Key Points:**
|
|
242
|
+
- `Node16` module resolution for proper ESM support
|
|
243
|
+
- `declaration: true` for type definitions
|
|
244
|
+
- `strict: true` for type safety
|
|
245
|
+
- Source maps for debugging
|
|
246
|
+
- `baseUrl` and `paths` for module name mapping (`@/` → `src/`)
|
|
247
|
+
|
|
248
|
+
### Jest Configuration
|
|
249
|
+
|
|
250
|
+
For projects with colocated tests (`.spec.ts` and `.e2e.ts` files alongside source code):
|
|
251
|
+
|
|
252
|
+
#### jest.config.js - Unit Tests
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
module.exports = {
|
|
256
|
+
preset: 'ts-jest',
|
|
257
|
+
testEnvironment: 'node',
|
|
258
|
+
roots: ['<rootDir>/src'],
|
|
259
|
+
testMatch: ['**/*.spec.ts'],
|
|
260
|
+
moduleFileExtensions: ['ts', 'js'],
|
|
261
|
+
collectCoverage: true,
|
|
262
|
+
coverageDirectory: 'coverage',
|
|
263
|
+
coverageReporters: ['text', 'lcov', 'html'],
|
|
264
|
+
collectCoverageFrom: [
|
|
265
|
+
'src/**/*.ts',
|
|
266
|
+
'!src/**/*.d.ts',
|
|
267
|
+
'!src/**/*.spec.ts',
|
|
268
|
+
'!src/**/*.e2e.ts',
|
|
269
|
+
'!src/index.ts', // Barrel export only
|
|
270
|
+
'!src/types/**/*.ts', // Type definitions only
|
|
271
|
+
],
|
|
272
|
+
moduleNameMapper: {
|
|
273
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### jest.e2e.config.js - E2E Tests
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
module.exports = {
|
|
282
|
+
preset: 'ts-jest',
|
|
283
|
+
testEnvironment: 'node',
|
|
284
|
+
testMatch: ['**/*.e2e.ts'],
|
|
285
|
+
testTimeout: 30000, // 30 seconds for real API calls
|
|
286
|
+
roots: ['<rootDir>/src'],
|
|
287
|
+
collectCoverageFrom: [
|
|
288
|
+
'src/**/*.ts',
|
|
289
|
+
'!src/**/*.spec.ts',
|
|
290
|
+
'!src/**/*.e2e.ts',
|
|
291
|
+
'!src/types/**/*.ts',
|
|
292
|
+
'!src/index.ts',
|
|
293
|
+
],
|
|
294
|
+
moduleNameMapper: {
|
|
295
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Key Points:**
|
|
301
|
+
- Separate configs for unit tests (`.spec.ts`) and e2e tests (`.e2e.ts`)
|
|
302
|
+
- E2E tests have longer timeout for real API calls
|
|
303
|
+
- Coverage excludes test files and type definitions
|
|
304
|
+
- `moduleNameMapper` matches TypeScript path aliases
|
|
305
|
+
- Tests are colocated with source files in `src/`
|
|
306
|
+
|
|
307
|
+
**Package.json Scripts:**
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"scripts": {
|
|
311
|
+
"test": "jest --config jest.config.js",
|
|
312
|
+
"test:e2e": "jest --config jest.e2e.config.js",
|
|
313
|
+
"test:watch": "jest --config jest.config.js --watch",
|
|
314
|
+
"test:coverage": "jest --config jest.config.js --coverage"
|
|
315
|
+
},
|
|
316
|
+
"devDependencies": {
|
|
317
|
+
"@types/jest": "^29.0.0",
|
|
318
|
+
"jest": "^29.0.0",
|
|
319
|
+
"ts-jest": "^29.0.0"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### esbuild.build.js Structure
|
|
325
|
+
|
|
326
|
+
For a simple MCP server (single entry point):
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
import * as esbuild from 'esbuild';
|
|
330
|
+
import { execSync } from 'child_process';
|
|
331
|
+
|
|
332
|
+
await esbuild.build({
|
|
333
|
+
entryPoints: ['src/server.ts'],
|
|
334
|
+
bundle: true,
|
|
335
|
+
platform: 'node',
|
|
336
|
+
target: 'node20',
|
|
337
|
+
format: 'esm',
|
|
338
|
+
outfile: 'dist/server.js',
|
|
339
|
+
sourcemap: true,
|
|
340
|
+
external: [
|
|
341
|
+
'weaviate-client',
|
|
342
|
+
'firebase-admin',
|
|
343
|
+
'@modelcontextprotocol/sdk'
|
|
344
|
+
],
|
|
345
|
+
banner: {
|
|
346
|
+
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);"
|
|
347
|
+
},
|
|
348
|
+
alias: {
|
|
349
|
+
'@': './src'
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
console.log('✓ JavaScript bundle built');
|
|
354
|
+
|
|
355
|
+
// Generate TypeScript declarations
|
|
356
|
+
console.log('Generating TypeScript declarations...');
|
|
357
|
+
try {
|
|
358
|
+
execSync('tsc --emitDeclarationOnly --outDir dist', { stdio: 'inherit' });
|
|
359
|
+
console.log('✓ TypeScript declarations generated');
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('✗ Failed to generate TypeScript declarations');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log('✓ Build complete');
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
For a library with multiple entry points:
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
import * as esbuild from 'esbuild';
|
|
372
|
+
import { execSync } from 'child_process';
|
|
373
|
+
import { readdir } from 'fs/promises';
|
|
374
|
+
import { join } from 'path';
|
|
375
|
+
|
|
376
|
+
// Option 1: Find all entry points dynamically
|
|
377
|
+
async function findEntryPoints(dir, base = 'src') {
|
|
378
|
+
const entries = [];
|
|
379
|
+
const files = await readdir(dir, { withFileTypes: true });
|
|
380
|
+
|
|
381
|
+
for (const file of files) {
|
|
382
|
+
const fullPath = join(dir, file.name);
|
|
383
|
+
if (file.isDirectory()) {
|
|
384
|
+
entries.push(...await findEntryPoints(fullPath, base));
|
|
385
|
+
} else if (file.name.endsWith('.ts') && !file.name.endsWith('.d.ts')) {
|
|
386
|
+
entries.push(fullPath);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return entries;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Option 2: Explicit entry points
|
|
394
|
+
const explicitEntryPoints = [
|
|
395
|
+
'src/server-factory.ts',
|
|
396
|
+
'src/client.ts',
|
|
397
|
+
'src/types.ts',
|
|
398
|
+
'src/tools/tool-one.ts',
|
|
399
|
+
'src/tools/tool-two.ts'
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
// Build CLI entry point (bundled)
|
|
403
|
+
await esbuild.build({
|
|
404
|
+
entryPoints: ['src/index.ts'],
|
|
405
|
+
bundle: true,
|
|
406
|
+
outfile: 'dist/index.js',
|
|
407
|
+
platform: 'node',
|
|
408
|
+
target: 'node18',
|
|
409
|
+
format: 'esm',
|
|
410
|
+
sourcemap: true,
|
|
411
|
+
external: [
|
|
412
|
+
'@modelcontextprotocol/sdk',
|
|
413
|
+
// Add other peer dependencies
|
|
414
|
+
],
|
|
415
|
+
banner: {
|
|
416
|
+
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);"
|
|
417
|
+
},
|
|
418
|
+
alias: {
|
|
419
|
+
'@': './src'
|
|
420
|
+
},
|
|
421
|
+
minify: false,
|
|
422
|
+
keepNames: true
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Build library exports (unbundled, preserves module structure)
|
|
426
|
+
await esbuild.build({
|
|
427
|
+
entryPoints: await findEntryPoints('src'), // or explicitEntryPoints
|
|
428
|
+
bundle: false, // Key: don't bundle for library
|
|
429
|
+
outdir: 'dist',
|
|
430
|
+
outbase: 'src', // Preserve directory structure
|
|
431
|
+
platform: 'node',
|
|
432
|
+
target: 'node18',
|
|
433
|
+
format: 'esm',
|
|
434
|
+
sourcemap: true,
|
|
435
|
+
alias: {
|
|
436
|
+
'@': './src'
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
console.log('✓ JavaScript bundles built');
|
|
441
|
+
|
|
442
|
+
// Generate TypeScript declarations
|
|
443
|
+
console.log('Generating TypeScript declarations...');
|
|
444
|
+
try {
|
|
445
|
+
execSync('tsc --emitDeclarationOnly --outDir dist', { stdio: 'inherit' });
|
|
446
|
+
console.log('✓ TypeScript declarations generated');
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error('✗ Failed to generate TypeScript declarations');
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log('✓ Build complete');
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Key Points:**
|
|
456
|
+
- **Simple servers**: Single bundled entry point
|
|
457
|
+
- **Library exports**: Dual build strategy (bundle CLI, preserve modules)
|
|
458
|
+
- `bundle: true` for standalone executable
|
|
459
|
+
- `bundle: false` + `outbase: 'src'` for library exports
|
|
460
|
+
- `external` array lists dependencies not to bundle (peer dependencies)
|
|
461
|
+
- `banner` adds CommonJS compatibility for ESM bundles
|
|
462
|
+
- `alias` enables path alias resolution (`@/` → `src/`)
|
|
463
|
+
- `target` specifies Node.js version compatibility
|
|
464
|
+
- Dynamic or explicit entry point discovery for libraries
|
|
465
|
+
- **TypeScript declarations**: Always run `tsc --emitDeclarationOnly` after esbuild
|
|
466
|
+
- **Why separate**: esbuild doesn't generate `.d.ts` files, TypeScript compiler does
|
|
467
|
+
- **Build order**: 1) esbuild bundles JS, 2) tsc generates types, 3) both in dist/
|
|
468
|
+
|
|
469
|
+
### esbuild.watch.js Structure
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
import * as esbuild from 'esbuild';
|
|
473
|
+
|
|
474
|
+
const ctx = await esbuild.context({
|
|
475
|
+
entryPoints: ['src/server.ts'],
|
|
476
|
+
bundle: true,
|
|
477
|
+
platform: 'node',
|
|
478
|
+
target: 'node20',
|
|
479
|
+
format: 'esm',
|
|
480
|
+
outfile: 'dist/server.js',
|
|
481
|
+
sourcemap: true,
|
|
482
|
+
external: [
|
|
483
|
+
'weaviate-client',
|
|
484
|
+
'firebase-admin',
|
|
485
|
+
'@modelcontextprotocol/sdk'
|
|
486
|
+
],
|
|
487
|
+
banner: {
|
|
488
|
+
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);"
|
|
489
|
+
},
|
|
490
|
+
alias: {
|
|
491
|
+
'@': './src'
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await ctx.watch();
|
|
496
|
+
console.log('👀 Watching for changes...');
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Key Points:**
|
|
500
|
+
- Uses `esbuild.context()` API for watch mode
|
|
501
|
+
- Same configuration as `esbuild.build.js` for consistency
|
|
502
|
+
- Automatically rebuilds on file changes
|
|
503
|
+
- Includes all the same options: `external`, `banner`, `alias`, etc.
|
|
504
|
+
|
|
505
|
+
## Source Code Patterns
|
|
506
|
+
|
|
507
|
+
### Tool Definition Pattern
|
|
508
|
+
|
|
509
|
+
Each tool file exports both definition and handler:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// src/tools/example-tool.ts
|
|
513
|
+
import { ClientWrapper } from '../client.js';
|
|
514
|
+
|
|
515
|
+
export const exampleTool = {
|
|
516
|
+
name: 'prefix_tool_name',
|
|
517
|
+
description: 'Clear description of what the tool does',
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: 'object',
|
|
520
|
+
properties: {
|
|
521
|
+
param1: {
|
|
522
|
+
type: 'string',
|
|
523
|
+
description: 'Parameter description'
|
|
524
|
+
},
|
|
525
|
+
param2: {
|
|
526
|
+
type: 'number',
|
|
527
|
+
description: 'Optional parameter',
|
|
528
|
+
default: 10
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
required: ['param1']
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
export async function handleExampleTool(
|
|
536
|
+
client: ClientWrapper,
|
|
537
|
+
args: any
|
|
538
|
+
): Promise<string> {
|
|
539
|
+
try {
|
|
540
|
+
const result = await client.doSomething(args.param1, args.param2);
|
|
541
|
+
return JSON.stringify(result, null, 2);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
throw new Error(`Failed to execute: ${error instanceof Error ? error.message : String(error)}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Key Points:**
|
|
549
|
+
- Tool definition is a plain object (MCP Tool schema)
|
|
550
|
+
- Handler is a separate async function
|
|
551
|
+
- Handler receives client instance and args
|
|
552
|
+
- Returns JSON string for MCP response
|
|
553
|
+
- Proper error handling
|
|
554
|
+
|
|
555
|
+
### Server Factory Pattern (Multi-Tenant)
|
|
556
|
+
|
|
557
|
+
For use with `mcp-auth` or other multi-tenant wrappers:
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// src/server-factory.ts
|
|
561
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
562
|
+
import { ClientWrapper } from './client.js';
|
|
563
|
+
import {
|
|
564
|
+
CallToolRequestSchema,
|
|
565
|
+
ListToolsRequestSchema,
|
|
566
|
+
ErrorCode,
|
|
567
|
+
McpError
|
|
568
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
569
|
+
|
|
570
|
+
// Import all tools
|
|
571
|
+
import { toolOne, handleToolOne } from './tools/tool-one.js';
|
|
572
|
+
import { toolTwo, handleToolTwo } from './tools/tool-two.js';
|
|
573
|
+
|
|
574
|
+
export interface ServerOptions {
|
|
575
|
+
name?: string;
|
|
576
|
+
version?: string;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Create a server instance for a specific user/tenant
|
|
581
|
+
*
|
|
582
|
+
* @param accessToken - User's access token for external API
|
|
583
|
+
* @param userId - User identifier
|
|
584
|
+
* @param options - Optional server configuration
|
|
585
|
+
* @returns Configured MCP Server instance
|
|
586
|
+
*/
|
|
587
|
+
export function createServer(
|
|
588
|
+
accessToken: string,
|
|
589
|
+
userId: string,
|
|
590
|
+
options: ServerOptions = {}
|
|
591
|
+
): Server {
|
|
592
|
+
if (!accessToken) {
|
|
593
|
+
throw new Error('accessToken is required');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!userId) {
|
|
597
|
+
throw new Error('userId is required');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Initialize client with user's credentials
|
|
601
|
+
const client = new ClientWrapper(accessToken);
|
|
602
|
+
|
|
603
|
+
// Create MCP server
|
|
604
|
+
const server = new Server(
|
|
605
|
+
{
|
|
606
|
+
name: options.name || 'mcp-server',
|
|
607
|
+
version: options.version || '1.0.0'
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
capabilities: {
|
|
611
|
+
tools: {}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Register list_tools handler
|
|
617
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
618
|
+
return {
|
|
619
|
+
tools: [
|
|
620
|
+
toolOne,
|
|
621
|
+
toolTwo,
|
|
622
|
+
// ... all tool definitions
|
|
623
|
+
]
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Register call_tool handler
|
|
628
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
629
|
+
const { name, arguments: args } = request.params;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
let result: string;
|
|
633
|
+
|
|
634
|
+
switch (name) {
|
|
635
|
+
case 'prefix_tool_one':
|
|
636
|
+
result = await handleToolOne(client, args);
|
|
637
|
+
break;
|
|
638
|
+
|
|
639
|
+
case 'prefix_tool_two':
|
|
640
|
+
result = await handleToolTwo(client, args);
|
|
641
|
+
break;
|
|
642
|
+
|
|
643
|
+
default:
|
|
644
|
+
throw new McpError(
|
|
645
|
+
ErrorCode.MethodNotFound,
|
|
646
|
+
`Unknown tool: ${name}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
content: [
|
|
652
|
+
{
|
|
653
|
+
type: 'text',
|
|
654
|
+
text: result
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof McpError) {
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
throw new McpError(
|
|
664
|
+
ErrorCode.InternalError,
|
|
665
|
+
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
return server;
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Key Points:**
|
|
675
|
+
- Factory function creates isolated server instances
|
|
676
|
+
- Each instance has its own client with user credentials
|
|
677
|
+
- No shared state between instances
|
|
678
|
+
- Compatible with `mcp-auth` wrapping pattern
|
|
679
|
+
|
|
680
|
+
### Standalone Server Pattern
|
|
681
|
+
|
|
682
|
+
For direct stdio usage without multi-tenancy:
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// src/server.ts
|
|
686
|
+
#!/usr/bin/env node
|
|
687
|
+
|
|
688
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
689
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
690
|
+
import {
|
|
691
|
+
CallToolRequestSchema,
|
|
692
|
+
ListToolsRequestSchema,
|
|
693
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
694
|
+
import { config } from 'dotenv';
|
|
695
|
+
import { ClientWrapper } from './client.js';
|
|
696
|
+
import { logger } from './utils/logger.js';
|
|
697
|
+
|
|
698
|
+
// Import tools
|
|
699
|
+
import { ToolOne } from './tools/tool-one.js';
|
|
700
|
+
import { ToolTwo } from './tools/tool-two.js';
|
|
701
|
+
|
|
702
|
+
// Load environment variables
|
|
703
|
+
config();
|
|
704
|
+
|
|
705
|
+
class MCPServer {
|
|
706
|
+
private server: Server;
|
|
707
|
+
private client: ClientWrapper;
|
|
708
|
+
private toolOne: ToolOne;
|
|
709
|
+
private toolTwo: ToolTwo;
|
|
710
|
+
|
|
711
|
+
constructor() {
|
|
712
|
+
// Initialize server
|
|
713
|
+
this.server = new Server(
|
|
714
|
+
{
|
|
715
|
+
name: 'mcp-server',
|
|
716
|
+
version: '1.0.0',
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
capabilities: {
|
|
720
|
+
tools: {},
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// Initialize client
|
|
726
|
+
const apiKey = process.env.API_KEY;
|
|
727
|
+
if (!apiKey) {
|
|
728
|
+
throw new Error('API_KEY environment variable is required');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this.client = new ClientWrapper(apiKey);
|
|
732
|
+
|
|
733
|
+
// Initialize tools
|
|
734
|
+
this.toolOne = new ToolOne(this.client);
|
|
735
|
+
this.toolTwo = new ToolTwo(this.client);
|
|
736
|
+
|
|
737
|
+
this.setupHandlers();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private setupHandlers(): void {
|
|
741
|
+
// List available tools
|
|
742
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
743
|
+
return {
|
|
744
|
+
tools: [
|
|
745
|
+
this.toolOne.getToolDefinition(),
|
|
746
|
+
this.toolTwo.getToolDefinition(),
|
|
747
|
+
],
|
|
748
|
+
};
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Handle tool calls
|
|
752
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
753
|
+
const { name, arguments: args } = request.params;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
let result: any;
|
|
757
|
+
|
|
758
|
+
switch (name) {
|
|
759
|
+
case 'prefix_tool_one':
|
|
760
|
+
result = await this.toolOne.execute(args);
|
|
761
|
+
break;
|
|
762
|
+
|
|
763
|
+
case 'prefix_tool_two':
|
|
764
|
+
result = await this.toolTwo.execute(args);
|
|
765
|
+
break;
|
|
766
|
+
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: 'text',
|
|
775
|
+
text: JSON.stringify(result, null, 2),
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
logger.error(`Tool execution failed for ${name}:`, error);
|
|
781
|
+
return {
|
|
782
|
+
content: [
|
|
783
|
+
{
|
|
784
|
+
type: 'text',
|
|
785
|
+
text: JSON.stringify({
|
|
786
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
787
|
+
tool: name
|
|
788
|
+
}, null, 2),
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
isError: true,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async start(): Promise<void> {
|
|
798
|
+
try {
|
|
799
|
+
logger.info('Starting MCP Server...');
|
|
800
|
+
|
|
801
|
+
// Connect to external service
|
|
802
|
+
await this.client.connect();
|
|
803
|
+
|
|
804
|
+
// Start MCP server with stdio transport
|
|
805
|
+
const transport = new StdioServerTransport();
|
|
806
|
+
await this.server.connect(transport);
|
|
807
|
+
|
|
808
|
+
// Don't log to stdout/stderr when using stdio transport
|
|
809
|
+
// It interferes with MCP JSON protocol
|
|
810
|
+
|
|
811
|
+
} catch (error) {
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async stop(): Promise<void> {
|
|
817
|
+
await this.server.close();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Handle graceful shutdown
|
|
822
|
+
const server = new MCPServer();
|
|
823
|
+
|
|
824
|
+
process.on('SIGINT', async () => {
|
|
825
|
+
await server.stop();
|
|
826
|
+
process.exit(0);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
process.on('SIGTERM', async () => {
|
|
830
|
+
await server.stop();
|
|
831
|
+
process.exit(0);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Start the server
|
|
835
|
+
server.start().catch((error) => {
|
|
836
|
+
process.exit(1);
|
|
837
|
+
});
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
**Key Points:**
|
|
841
|
+
- Class-based server for encapsulation
|
|
842
|
+
- Environment variable configuration
|
|
843
|
+
- Graceful shutdown handling
|
|
844
|
+
- **Critical**: No stdout/stderr logging when using stdio transport
|
|
845
|
+
- Tool instances as class properties
|
|
846
|
+
|
|
847
|
+
### Client Wrapper Pattern
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// src/client.ts
|
|
851
|
+
export interface ClientConfig {
|
|
852
|
+
apiKey: string;
|
|
853
|
+
baseUrl?: string;
|
|
854
|
+
timeout?: number;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export class ClientWrapper {
|
|
858
|
+
private config: ClientConfig;
|
|
859
|
+
private isConnected = false;
|
|
860
|
+
|
|
861
|
+
constructor(apiKey: string, options?: Partial<ClientConfig>) {
|
|
862
|
+
this.config = {
|
|
863
|
+
apiKey,
|
|
864
|
+
baseUrl: options?.baseUrl || 'https://api.example.com',
|
|
865
|
+
timeout: options?.timeout || 30000
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async connect(): Promise<void> {
|
|
870
|
+
// Initialize connection, validate credentials, etc.
|
|
871
|
+
this.isConnected = true;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async doSomething(param: string): Promise<any> {
|
|
875
|
+
if (!this.isConnected) {
|
|
876
|
+
throw new Error('Client not connected');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Make API call
|
|
880
|
+
const response = await fetch(`${this.config.baseUrl}/endpoint`, {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
headers: {
|
|
883
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
884
|
+
'Content-Type': 'application/json'
|
|
885
|
+
},
|
|
886
|
+
body: JSON.stringify({ param })
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return response.json();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
isClientConnected(): boolean {
|
|
897
|
+
return this.isConnected;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
**Key Points:**
|
|
903
|
+
- Encapsulates external API communication
|
|
904
|
+
- Accepts credentials in constructor (for multi-tenant)
|
|
905
|
+
- Connection state management
|
|
906
|
+
- Error handling
|
|
907
|
+
|
|
908
|
+
### Logger Pattern (Stdio-Safe)
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
// src/utils/logger.ts
|
|
912
|
+
export enum LogLevel {
|
|
913
|
+
ERROR = 0,
|
|
914
|
+
WARN = 1,
|
|
915
|
+
INFO = 2,
|
|
916
|
+
DEBUG = 3
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
class Logger {
|
|
920
|
+
// No-op logger to avoid interfering with stdio MCP transport
|
|
921
|
+
// All logging methods do nothing to prevent JSON corruption
|
|
922
|
+
|
|
923
|
+
error(message: string, ...args: any[]): void {
|
|
924
|
+
// No-op when using stdio
|
|
925
|
+
// Could write to file or use process.stderr in non-stdio mode
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
warn(message: string, ...args: any[]): void {
|
|
929
|
+
// No-op
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
info(message: string, ...args: any[]): void {
|
|
933
|
+
// No-op
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
debug(message: string, ...args: any[]): void {
|
|
937
|
+
// No-op
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export const logger = new Logger();
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Key Points:**
|
|
945
|
+
- **Critical**: No console output when using stdio transport
|
|
946
|
+
- Stdio transport uses stdout/stdin for JSON-RPC
|
|
947
|
+
- Any console output corrupts the protocol
|
|
948
|
+
- Alternative: Write to file or use stderr carefully
|
|
949
|
+
|
|
950
|
+
### Error Serializer Pattern
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
// src/utils/error-serializer.ts
|
|
954
|
+
export function serializeError(error: unknown): any {
|
|
955
|
+
if (error instanceof Error) {
|
|
956
|
+
return {
|
|
957
|
+
name: error.name,
|
|
958
|
+
message: error.message,
|
|
959
|
+
stack: error.stack,
|
|
960
|
+
...(error as any) // Include any additional properties
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
message: String(error)
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
## Integration with mcp-auth
|
|
971
|
+
|
|
972
|
+
### Using AuthenticatedMCPServer
|
|
973
|
+
|
|
974
|
+
If building a new server with tool-level auth:
|
|
975
|
+
|
|
976
|
+
```typescript
|
|
977
|
+
// src/index.ts
|
|
978
|
+
import { AuthenticatedMCPServer } from '@prmichaelsen/mcp-auth/server';
|
|
979
|
+
import { EnvAuthProvider } from '@prmichaelsen/mcp-auth/providers/env';
|
|
980
|
+
import { SimpleTokenResolver } from '@prmichaelsen/mcp-auth';
|
|
981
|
+
import { withAuth } from '@prmichaelsen/mcp-auth/server';
|
|
982
|
+
|
|
983
|
+
const server = new AuthenticatedMCPServer({
|
|
984
|
+
name: 'my-server',
|
|
985
|
+
authProvider: new EnvAuthProvider(),
|
|
986
|
+
tokenResolver: new SimpleTokenResolver({ tokenEnvVar: 'API_TOKEN' }),
|
|
987
|
+
resourceType: 'myapi',
|
|
988
|
+
transport: { type: 'stdio' }
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
server.registerTool('get_data', withAuth(async (args, accessToken, userId) => {
|
|
992
|
+
const client = new ClientWrapper(accessToken);
|
|
993
|
+
return client.getData(args);
|
|
994
|
+
}));
|
|
995
|
+
|
|
996
|
+
await server.start();
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### Using Server Wrapping Pattern
|
|
1000
|
+
|
|
1001
|
+
If wrapping an existing server factory:
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
// Wrapper server using mcp-auth
|
|
1005
|
+
import { wrapServer } from '@prmichaelsen/mcp-auth/wrapper';
|
|
1006
|
+
import { createServer } from './server-factory.js';
|
|
1007
|
+
|
|
1008
|
+
const wrappedServer = wrapServer({
|
|
1009
|
+
serverFactory: createServer,
|
|
1010
|
+
authProvider: new JWTAuthProvider({ secret: process.env.JWT_SECRET }),
|
|
1011
|
+
tokenResolver: new APITokenResolver({ apiUrl: process.env.API_URL }),
|
|
1012
|
+
resourceType: 'myapi',
|
|
1013
|
+
transport: { type: 'sse', port: 3000 }
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
await wrappedServer.start();
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
## Directory Organization Best Practices
|
|
1020
|
+
|
|
1021
|
+
### Agent Directory
|
|
1022
|
+
|
|
1023
|
+
The `agent/` directory contains documentation and planning:
|
|
1024
|
+
|
|
1025
|
+
```
|
|
1026
|
+
agent/
|
|
1027
|
+
├── patterns/ # Architecture patterns
|
|
1028
|
+
│ ├── bootstrap.md # This document
|
|
1029
|
+
│ ├── library-services.md # Service layer patterns
|
|
1030
|
+
│ └── ...
|
|
1031
|
+
│
|
|
1032
|
+
├── tasks/ # Task tracking
|
|
1033
|
+
│ ├── task-001.md
|
|
1034
|
+
│ └── ...
|
|
1035
|
+
│
|
|
1036
|
+
├── milestones/ # Milestone planning
|
|
1037
|
+
│ ├── milestone-1.md
|
|
1038
|
+
│ └── ...
|
|
1039
|
+
│
|
|
1040
|
+
├── progress.yaml # Progress tracking
|
|
1041
|
+
└── requirements.md # Requirements document
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Types Organization
|
|
1045
|
+
|
|
1046
|
+
Types can be organized in two ways:
|
|
1047
|
+
|
|
1048
|
+
**Option 1: Flat structure** (simple projects)
|
|
1049
|
+
```
|
|
1050
|
+
src/
|
|
1051
|
+
├── types.ts # All types in one file
|
|
1052
|
+
└── ...
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
**Option 2: Types directory** (complex projects)
|
|
1056
|
+
```
|
|
1057
|
+
src/
|
|
1058
|
+
├── types/
|
|
1059
|
+
│ ├── mcp.ts # MCP-specific types
|
|
1060
|
+
│ ├── api.ts # External API types
|
|
1061
|
+
│ ├── domain.ts # Domain types
|
|
1062
|
+
│ └── index.ts # Re-exports
|
|
1063
|
+
└── ...
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### Utils Organization
|
|
1067
|
+
|
|
1068
|
+
```
|
|
1069
|
+
src/
|
|
1070
|
+
├── utils/
|
|
1071
|
+
│ ├── logger.ts # Logging utility
|
|
1072
|
+
│ ├── error-serializer.ts # Error handling
|
|
1073
|
+
│ ├── validation.ts # Input validation
|
|
1074
|
+
│ └── index.ts # Re-exports
|
|
1075
|
+
└── ...
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
## Build Output Structure
|
|
1079
|
+
|
|
1080
|
+
After building, the output should mirror the source structure:
|
|
1081
|
+
|
|
1082
|
+
```
|
|
1083
|
+
dist/
|
|
1084
|
+
├── index.js # Bundled CLI entry
|
|
1085
|
+
├── index.d.ts
|
|
1086
|
+
├── server-factory.js # Unbundled library exports
|
|
1087
|
+
├── server-factory.d.ts
|
|
1088
|
+
├── client.js
|
|
1089
|
+
├── client.d.ts
|
|
1090
|
+
├── types.js
|
|
1091
|
+
├── types.d.ts
|
|
1092
|
+
├── tools/
|
|
1093
|
+
│ ├── tool-one.js
|
|
1094
|
+
│ ├── tool-one.d.ts
|
|
1095
|
+
│ ├── tool-two.js
|
|
1096
|
+
│ ├── tool-two.d.ts
|
|
1097
|
+
│ └── index.js
|
|
1098
|
+
└── utils/
|
|
1099
|
+
├── logger.js
|
|
1100
|
+
├── logger.d.ts
|
|
1101
|
+
└── ...
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**Key Points:**
|
|
1105
|
+
- `index.js` is bundled (single file)
|
|
1106
|
+
- Other exports preserve module structure
|
|
1107
|
+
- Type definitions (`.d.ts`) for all modules
|
|
1108
|
+
- Source maps (`.js.map`) for debugging
|
|
1109
|
+
|
|
1110
|
+
## Import Patterns
|
|
1111
|
+
|
|
1112
|
+
### ESM Import Extensions
|
|
1113
|
+
|
|
1114
|
+
Always include `.js` extension in imports (even for `.ts` files):
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
// ✅ Correct
|
|
1118
|
+
import { ClientWrapper } from './client.js';
|
|
1119
|
+
import { toolOne } from './tools/tool-one.js';
|
|
1120
|
+
|
|
1121
|
+
// ❌ Wrong
|
|
1122
|
+
import { ClientWrapper } from './client';
|
|
1123
|
+
import { toolOne } from './tools/tool-one';
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Re-export Patterns
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
// src/tools/index.ts
|
|
1130
|
+
export * from './tool-one.js';
|
|
1131
|
+
export * from './tool-two.js';
|
|
1132
|
+
|
|
1133
|
+
// Usage
|
|
1134
|
+
import { toolOne, toolTwo } from './tools/index.js';
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
## Environment Configuration
|
|
1138
|
+
|
|
1139
|
+
### .env.example
|
|
1140
|
+
|
|
1141
|
+
```bash
|
|
1142
|
+
# API Configuration
|
|
1143
|
+
API_KEY=your_api_key_here
|
|
1144
|
+
API_URL=https://api.example.com
|
|
1145
|
+
|
|
1146
|
+
# Server Configuration
|
|
1147
|
+
PORT=3000
|
|
1148
|
+
NODE_ENV=development
|
|
1149
|
+
|
|
1150
|
+
# Logging
|
|
1151
|
+
LOG_LEVEL=info
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### Environment Loading
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
import { config } from 'dotenv';
|
|
1158
|
+
|
|
1159
|
+
// Load at server startup
|
|
1160
|
+
config();
|
|
1161
|
+
|
|
1162
|
+
// Access variables
|
|
1163
|
+
const apiKey = process.env.API_KEY;
|
|
1164
|
+
if (!apiKey) {
|
|
1165
|
+
throw new Error('API_KEY is required');
|
|
1166
|
+
}
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
## Testing Considerations
|
|
1170
|
+
|
|
1171
|
+
While not covered in detail, consider:
|
|
1172
|
+
|
|
1173
|
+
```
|
|
1174
|
+
src/
|
|
1175
|
+
├── tools/
|
|
1176
|
+
│ ├── tool-one.ts
|
|
1177
|
+
│ ├── tool-one.test.ts # Co-located tests
|
|
1178
|
+
│ └── ...
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
Or separate test directory:
|
|
1182
|
+
|
|
1183
|
+
```
|
|
1184
|
+
tests/
|
|
1185
|
+
├── tools/
|
|
1186
|
+
│ ├── tool-one.test.ts
|
|
1187
|
+
│ └── ...
|
|
1188
|
+
└── integration/
|
|
1189
|
+
└── ...
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
## Common Patterns Summary
|
|
1193
|
+
|
|
1194
|
+
### 1. Tool Organization
|
|
1195
|
+
- One file per tool
|
|
1196
|
+
- Export definition and handler separately
|
|
1197
|
+
- Handler receives client and args
|
|
1198
|
+
- Return JSON strings
|
|
1199
|
+
|
|
1200
|
+
### 2. Server Patterns
|
|
1201
|
+
- **Factory**: For multi-tenant (returns Server instance)
|
|
1202
|
+
- **Class**: For standalone (manages lifecycle)
|
|
1203
|
+
- Both patterns supported
|
|
1204
|
+
|
|
1205
|
+
### 3. Build Strategy
|
|
1206
|
+
- **Bundle**: CLI entry point (single file)
|
|
1207
|
+
- **Preserve**: Library exports (module structure)
|
|
1208
|
+
- TypeScript declarations always generated
|
|
1209
|
+
|
|
1210
|
+
### 4. Client Pattern
|
|
1211
|
+
- Wrapper class for external API
|
|
1212
|
+
- Accept credentials in constructor
|
|
1213
|
+
- Stateful connection management
|
|
1214
|
+
|
|
1215
|
+
### 5. Logging Pattern
|
|
1216
|
+
- No-op for stdio transport
|
|
1217
|
+
- File or stderr for other transports
|
|
1218
|
+
- Never use console.log with stdio
|
|
1219
|
+
|
|
1220
|
+
### 6. Type Safety
|
|
1221
|
+
- Strong typing throughout
|
|
1222
|
+
- Separate type files or directories
|
|
1223
|
+
- Export types for library consumers
|
|
1224
|
+
|
|
1225
|
+
### 7. Error Handling
|
|
1226
|
+
- Serialize errors for MCP responses
|
|
1227
|
+
- Proper error types (McpError)
|
|
1228
|
+
- Graceful degradation
|
|
1229
|
+
|
|
1230
|
+
## Compatibility Checklist
|
|
1231
|
+
|
|
1232
|
+
When building a server compatible with `mcp-auth`:
|
|
1233
|
+
|
|
1234
|
+
- ✅ Export a factory function that accepts `(accessToken, userId, options?)`
|
|
1235
|
+
- ✅ Factory returns a configured `Server` instance
|
|
1236
|
+
- ✅ No shared state between server instances
|
|
1237
|
+
- ✅ Client wrapper accepts credentials in constructor
|
|
1238
|
+
- ✅ Tools are stateless (receive client as parameter)
|
|
1239
|
+
- ✅ Proper TypeScript types exported
|
|
1240
|
+
- ✅ ESM with `.js` extensions in imports
|
|
1241
|
+
- ✅ Dual build: bundled CLI + preserved modules
|
|
1242
|
+
|
|
1243
|
+
## Migration Path
|
|
1244
|
+
|
|
1245
|
+
### From Standalone to Multi-Tenant
|
|
1246
|
+
|
|
1247
|
+
1. Extract server creation into factory function
|
|
1248
|
+
2. Move credential loading from env to factory parameters
|
|
1249
|
+
3. Ensure no shared state between instances
|
|
1250
|
+
4. Add factory export to package.json
|
|
1251
|
+
5. Update build to preserve module structure
|
|
1252
|
+
|
|
1253
|
+
### From Multi-Tenant to mcp-auth Integration
|
|
1254
|
+
|
|
1255
|
+
1. Keep existing factory function
|
|
1256
|
+
2. Add mcp-auth wrapper in separate entry point
|
|
1257
|
+
3. Configure auth provider and token resolver
|
|
1258
|
+
4. Deploy wrapped server for remote access
|
|
1259
|
+
5. Keep factory for direct usage
|
|
1260
|
+
|
|
1261
|
+
## Conclusion
|
|
1262
|
+
|
|
1263
|
+
This bootstrap pattern provides a foundation for building MCP servers that are:
|
|
1264
|
+
|
|
1265
|
+
- **Modular**: Clear separation of concerns
|
|
1266
|
+
- **Type-safe**: Strong TypeScript typing
|
|
1267
|
+
- **Multi-tenant ready**: Isolated instances per user
|
|
1268
|
+
- **Library-friendly**: Dual build strategy
|
|
1269
|
+
- **mcp-auth compatible**: Works with authentication framework
|
|
1270
|
+
|
|
1271
|
+
The pattern emphasizes **organization and structure** over specific implementations, allowing flexibility in choosing tools and technologies while maintaining consistency and compatibility.
|