mcp-http-webhook 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +8 -0
- package/ARCHITECTURE.md +269 -0
- package/CONTRIBUTING.md +136 -0
- package/GETTING_STARTED.md +310 -0
- package/IMPLEMENTATION.md +294 -0
- package/LICENSE +21 -0
- package/MIGRATION_TO_SDK.md +263 -0
- package/README.md +496 -0
- package/SDK_INTEGRATION_COMPLETE.md +300 -0
- package/STANDARD_SUBSCRIPTIONS.md +268 -0
- package/STANDARD_SUBSCRIPTIONS_COMPLETE.md +309 -0
- package/SUMMARY.md +272 -0
- package/Spec.md +2778 -0
- package/dist/errors/index.d.ts +52 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +81 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ProtocolHandler.d.ts +37 -0
- package/dist/protocol/ProtocolHandler.d.ts.map +1 -0
- package/dist/protocol/ProtocolHandler.js +172 -0
- package/dist/protocol/ProtocolHandler.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +502 -0
- package/dist/server.js.map +1 -0
- package/dist/stores/InMemoryStore.d.ts +27 -0
- package/dist/stores/InMemoryStore.d.ts.map +1 -0
- package/dist/stores/InMemoryStore.js +73 -0
- package/dist/stores/InMemoryStore.js.map +1 -0
- package/dist/stores/RedisStore.d.ts +18 -0
- package/dist/stores/RedisStore.d.ts.map +1 -0
- package/dist/stores/RedisStore.js +45 -0
- package/dist/stores/RedisStore.js.map +1 -0
- package/dist/stores/index.d.ts +3 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +9 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/subscriptions/SubscriptionManager.d.ts +49 -0
- package/dist/subscriptions/SubscriptionManager.d.ts.map +1 -0
- package/dist/subscriptions/SubscriptionManager.js +181 -0
- package/dist/subscriptions/SubscriptionManager.js.map +1 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +51 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +154 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/webhooks/WebhookManager.d.ts +27 -0
- package/dist/webhooks/WebhookManager.d.ts.map +1 -0
- package/dist/webhooks/WebhookManager.js +174 -0
- package/dist/webhooks/WebhookManager.js.map +1 -0
- package/examples/GITHUB_LIVE_EXAMPLE.md +308 -0
- package/examples/GITHUB_LIVE_SETUP.md +253 -0
- package/examples/QUICKSTART.md +130 -0
- package/examples/basic-setup.ts +142 -0
- package/examples/github-server-live.ts +690 -0
- package/examples/github-server.ts +223 -0
- package/examples/google-drive-server-live.ts +773 -0
- package/examples/start-github-live.sh +53 -0
- package/jest.config.js +20 -0
- package/package.json +58 -0
- package/src/errors/index.ts +81 -0
- package/src/index.ts +19 -0
- package/src/server.ts +595 -0
- package/src/stores/InMemoryStore.ts +87 -0
- package/src/stores/RedisStore.ts +51 -0
- package/src/stores/index.ts +2 -0
- package/src/subscriptions/SubscriptionManager.ts +240 -0
- package/src/types/index.ts +341 -0
- package/src/utils/index.ts +156 -0
- package/src/webhooks/WebhookManager.ts +230 -0
- package/test-sdk-integration.sh +157 -0
- package/tsconfig.json +21 -0
package/Spec.md
ADDED
|
@@ -0,0 +1,2778 @@
|
|
|
1
|
+
# MCP HTTP Webhook Server Library - Technical Specification
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0.0
|
|
4
|
+
**Target SDK:** Model Context Protocol TypeScript SDK
|
|
5
|
+
**Transport Method:** HTTP + Webhooks (No SSE)
|
|
6
|
+
**Reference:** [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Executive Summary
|
|
11
|
+
|
|
12
|
+
This library provides an opinionated, production-ready framework for building MCP servers that operate entirely over HTTP with webhook-based resource subscriptions. Unlike the standard MCP SSE transport, this library eliminates persistent connections in favor of stateless HTTP requests and webhook callbacks.
|
|
13
|
+
|
|
14
|
+
**Key Value Propositions:**
|
|
15
|
+
- **Horizontally Scalable:** No connection state = trivial multi-instance deployment
|
|
16
|
+
- **Third-Party Integration:** Native webhook support for GitHub, Google Drive, Slack, PostgreSQL, etc.
|
|
17
|
+
- **Client Flexibility:** Clients provide their own webhook endpoints
|
|
18
|
+
- **Production Ready:** Built-in retry logic, signature verification, and persistent storage
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Architecture Overview
|
|
23
|
+
|
|
24
|
+
### 2.1 Core Components
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
28
|
+
│ MCP HTTP Server │
|
|
29
|
+
├─────────────────────────────────────────────────────────────┤
|
|
30
|
+
│ │
|
|
31
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
32
|
+
│ │ Protocol │ │ Webhook │ │ Subscription │ │
|
|
33
|
+
│ │ Handler │ │ Manager │ │ Manager │ │
|
|
34
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
35
|
+
│ │
|
|
36
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
37
|
+
│ │ Key-Value Store Interface │ │
|
|
38
|
+
│ │ (Redis / DynamoDB / PostgreSQL) │ │
|
|
39
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
40
|
+
│ │
|
|
41
|
+
└─────────────────────────────────────────────────────────────┘
|
|
42
|
+
▲ ▲ ▲
|
|
43
|
+
│ │ │
|
|
44
|
+
HTTP Requests Third-Party Client
|
|
45
|
+
(Tools/Resources) Webhooks Webhooks
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2.2 Data Flow
|
|
49
|
+
|
|
50
|
+
**Standard Tool/Resource Access:**
|
|
51
|
+
```
|
|
52
|
+
Client → HTTP POST → MCP Server → Tool/Resource Handler → Response
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Subscription Flow:**
|
|
56
|
+
```
|
|
57
|
+
1. Client → POST /resources/subscribe (with callbackUrl)
|
|
58
|
+
2. MCP Server → Generates subscriptionId
|
|
59
|
+
3. MCP Server → Creates webhook URL for third-party
|
|
60
|
+
4. MCP Server → Calls onSubscribe handler
|
|
61
|
+
5. Handler → Registers webhook with third-party service
|
|
62
|
+
6. MCP Server → Stores subscription in KV store
|
|
63
|
+
7. MCP Server → Returns subscriptionId to client
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Notification Flow:**
|
|
67
|
+
```
|
|
68
|
+
1. Third-Party → POST /webhooks/incoming/{subscriptionId}
|
|
69
|
+
2. MCP Server → Loads subscription from KV store
|
|
70
|
+
3. MCP Server → Calls onWebhook handler
|
|
71
|
+
4. Handler → Parses payload, returns change info
|
|
72
|
+
5. MCP Server → HTTP POST to client callbackUrl
|
|
73
|
+
6. Client → Receives notification with subscriptionId
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. Installation & Setup
|
|
79
|
+
|
|
80
|
+
### 3.1 Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install mcp-http-webhook
|
|
84
|
+
# or
|
|
85
|
+
pnpm add mcp-http-webhook
|
|
86
|
+
# or
|
|
87
|
+
yarn add mcp-http-webhook
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3.2 Peer Dependencies
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
95
|
+
"express": "^4.18.0",
|
|
96
|
+
"ioredis": "^5.3.0"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4. Core API Reference
|
|
103
|
+
|
|
104
|
+
### 4.1 Server Creation
|
|
105
|
+
|
|
106
|
+
**Function:** `createMCPServer(config: MCPServerConfig): MCPServer`
|
|
107
|
+
|
|
108
|
+
**Configuration Interface:**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
interface MCPServerConfig {
|
|
112
|
+
// Server Identity (MCP Protocol)
|
|
113
|
+
name: string;
|
|
114
|
+
version: string;
|
|
115
|
+
|
|
116
|
+
// HTTP Server Configuration
|
|
117
|
+
port?: number; // Default: 3000
|
|
118
|
+
host?: string; // Default: '0.0.0.0'
|
|
119
|
+
basePath?: string; // Default: '/mcp'
|
|
120
|
+
publicUrl: string; // Required: e.g., 'https://mcp.example.com'
|
|
121
|
+
|
|
122
|
+
// Authentication
|
|
123
|
+
authenticate?: (req: Request) => Promise<AuthContext>;
|
|
124
|
+
|
|
125
|
+
// Core MCP Components
|
|
126
|
+
tools: ToolDefinition[];
|
|
127
|
+
resources: ResourceDefinition[];
|
|
128
|
+
prompts?: PromptDefinition[];
|
|
129
|
+
|
|
130
|
+
// Storage (Required for subscriptions)
|
|
131
|
+
store: KeyValueStore;
|
|
132
|
+
|
|
133
|
+
// Webhook Configuration
|
|
134
|
+
webhooks?: WebhookConfig;
|
|
135
|
+
|
|
136
|
+
// Logging
|
|
137
|
+
logger?: Logger;
|
|
138
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Reference Implementation:**
|
|
143
|
+
See [examples/basic-setup.ts](#)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 5. Tool Definition
|
|
148
|
+
|
|
149
|
+
Tools follow the standard MCP tool specification with HTTP transport.
|
|
150
|
+
|
|
151
|
+
**Interface:**
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
interface ToolDefinition<TInput = any, TOutput = any> {
|
|
155
|
+
name: string;
|
|
156
|
+
description: string;
|
|
157
|
+
inputSchema: JSONSchema;
|
|
158
|
+
handler: (input: TInput, context: AuthContext) => Promise<TOutput>;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Example:**
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
{
|
|
166
|
+
name: 'create_github_issue',
|
|
167
|
+
description: 'Creates a new issue in a GitHub repository',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
owner: { type: 'string', description: 'Repository owner' },
|
|
172
|
+
repo: { type: 'string', description: 'Repository name' },
|
|
173
|
+
title: { type: 'string', description: 'Issue title' },
|
|
174
|
+
body: { type: 'string', description: 'Issue body' }
|
|
175
|
+
},
|
|
176
|
+
required: ['owner', 'repo', 'title']
|
|
177
|
+
},
|
|
178
|
+
handler: async (input, context) => {
|
|
179
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
180
|
+
const { data } = await octokit.issues.create(input);
|
|
181
|
+
return { issue: data };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**HTTP Endpoint:** `POST /mcp/tools/call`
|
|
187
|
+
|
|
188
|
+
**Request Format:**
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"method": "tools/call",
|
|
192
|
+
"params": {
|
|
193
|
+
"name": "create_github_issue",
|
|
194
|
+
"arguments": {
|
|
195
|
+
"owner": "octocat",
|
|
196
|
+
"repo": "hello-world",
|
|
197
|
+
"title": "Bug found"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Reference:**
|
|
204
|
+
- [MCP Tool Specification](https://spec.modelcontextprotocol.io/specification/server/tools/)
|
|
205
|
+
- [Tool Examples](#)
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 6. Resource Definition
|
|
210
|
+
|
|
211
|
+
Resources represent data that can be read and optionally subscribed to.
|
|
212
|
+
|
|
213
|
+
### 6.1 Basic Resource
|
|
214
|
+
|
|
215
|
+
**Interface:**
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
interface ResourceDefinition<TData = any> {
|
|
219
|
+
uri: string; // URI template, e.g., "github://repo/{owner}/{repo}/issues"
|
|
220
|
+
name: string;
|
|
221
|
+
description: string;
|
|
222
|
+
mimeType?: string;
|
|
223
|
+
|
|
224
|
+
read: (uri: string, context: AuthContext) => Promise<{
|
|
225
|
+
contents: TData;
|
|
226
|
+
metadata?: Record<string, any>;
|
|
227
|
+
}>;
|
|
228
|
+
|
|
229
|
+
list?: (context: AuthContext) => Promise<ResourceListItem[]>;
|
|
230
|
+
|
|
231
|
+
subscription?: ResourceSubscription;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Example:**
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
{
|
|
239
|
+
uri: 'github://repo/{owner}/{repo}/issues',
|
|
240
|
+
name: 'GitHub Repository Issues',
|
|
241
|
+
description: 'List of all issues in a repository',
|
|
242
|
+
mimeType: 'application/json',
|
|
243
|
+
|
|
244
|
+
read: async (uri, context) => {
|
|
245
|
+
const { owner, repo } = parseUriTemplate(uri);
|
|
246
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
247
|
+
const { data } = await octokit.issues.listForRepo({ owner, repo });
|
|
248
|
+
return { contents: data };
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
list: async (context) => {
|
|
252
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
253
|
+
const { data } = await octokit.repos.listForAuthenticatedUser();
|
|
254
|
+
return data.map(repo => ({
|
|
255
|
+
uri: `github://repo/${repo.owner.login}/${repo.name}/issues`,
|
|
256
|
+
name: `${repo.full_name} Issues`
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**HTTP Endpoints:**
|
|
263
|
+
- `POST /mcp/resources/list`
|
|
264
|
+
- `POST /mcp/resources/read`
|
|
265
|
+
|
|
266
|
+
**Reference:**
|
|
267
|
+
- [MCP Resource Specification](https://spec.modelcontextprotocol.io/specification/server/resources/)
|
|
268
|
+
- [Resource Examples](#)
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 7. Webhook-Based Subscriptions
|
|
273
|
+
|
|
274
|
+
This is the core differentiator of this library.
|
|
275
|
+
|
|
276
|
+
### 7.1 Subscription Interface
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
interface ResourceSubscription {
|
|
280
|
+
/**
|
|
281
|
+
* Called when a client subscribes to a resource
|
|
282
|
+
*
|
|
283
|
+
* @param uri - Resource URI being subscribed to
|
|
284
|
+
* @param subscriptionId - Unique ID (generated by library)
|
|
285
|
+
* @param thirdPartyWebhookUrl - URL to give to third-party service
|
|
286
|
+
* @param context - Authentication context
|
|
287
|
+
* @returns Metadata to persist (webhook IDs, etc.)
|
|
288
|
+
*/
|
|
289
|
+
onSubscribe: (
|
|
290
|
+
uri: string,
|
|
291
|
+
subscriptionId: string,
|
|
292
|
+
thirdPartyWebhookUrl: string,
|
|
293
|
+
context: AuthContext
|
|
294
|
+
) => Promise<SubscriptionMetadata>;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Called when a client unsubscribes
|
|
298
|
+
*
|
|
299
|
+
* @param uri - Resource URI
|
|
300
|
+
* @param subscriptionId - Subscription ID
|
|
301
|
+
* @param storedData - Data from onSubscribe
|
|
302
|
+
* @param context - Authentication context
|
|
303
|
+
*/
|
|
304
|
+
onUnsubscribe: (
|
|
305
|
+
uri: string,
|
|
306
|
+
subscriptionId: string,
|
|
307
|
+
storedData: SubscriptionMetadata,
|
|
308
|
+
context: AuthContext
|
|
309
|
+
) => Promise<void>;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Called when third-party webhook is received
|
|
313
|
+
*
|
|
314
|
+
* @param subscriptionId - From webhook URL path
|
|
315
|
+
* @param payload - Webhook payload
|
|
316
|
+
* @param headers - HTTP headers (for signature verification)
|
|
317
|
+
* @returns Change information or null
|
|
318
|
+
*/
|
|
319
|
+
onWebhook: (
|
|
320
|
+
subscriptionId: string,
|
|
321
|
+
payload: any,
|
|
322
|
+
headers: Record<string, string>
|
|
323
|
+
) => Promise<WebhookChangeInfo | null>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface SubscriptionMetadata {
|
|
327
|
+
thirdPartyWebhookId: string;
|
|
328
|
+
metadata?: Record<string, any>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface WebhookChangeInfo {
|
|
332
|
+
resourceUri: string;
|
|
333
|
+
changeType: 'created' | 'updated' | 'deleted';
|
|
334
|
+
data?: any;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 7.2 Subscription Lifecycle
|
|
339
|
+
|
|
340
|
+
**Step 1: Client Subscribes**
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
POST /mcp/resources/subscribe
|
|
344
|
+
Authorization: Bearer <token>
|
|
345
|
+
Content-Type: application/json
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
"method": "resources/subscribe",
|
|
349
|
+
"params": {
|
|
350
|
+
"uri": "github://repo/octocat/hello-world/issues",
|
|
351
|
+
"callbackUrl": "https://client.example.com/webhooks/mcp",
|
|
352
|
+
"callbackSecret": "client-webhook-secret"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Response:
|
|
357
|
+
{
|
|
358
|
+
"subscriptionId": "sub_xyz789",
|
|
359
|
+
"status": "active"
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Internal Flow:**
|
|
364
|
+
1. Library generates `subscriptionId`
|
|
365
|
+
2. Library creates `thirdPartyWebhookUrl = {publicUrl}/webhooks/incoming/{subscriptionId}`
|
|
366
|
+
3. Library calls `onSubscribe(uri, subscriptionId, thirdPartyWebhookUrl, context)`
|
|
367
|
+
4. Handler registers webhook with third-party (GitHub, etc.)
|
|
368
|
+
5. Library stores subscription data in KV store
|
|
369
|
+
6. Library returns `subscriptionId` to client
|
|
370
|
+
|
|
371
|
+
**Step 2: Third-Party Notifies MCP Server**
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
POST /webhooks/incoming/sub_xyz789
|
|
375
|
+
X-GitHub-Event: issues
|
|
376
|
+
X-Hub-Signature-256: sha256=...
|
|
377
|
+
Content-Type: application/json
|
|
378
|
+
|
|
379
|
+
{
|
|
380
|
+
"action": "opened",
|
|
381
|
+
"issue": { ... },
|
|
382
|
+
"repository": { ... }
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Internal Flow:**
|
|
387
|
+
1. Library extracts `subscriptionId` from URL path
|
|
388
|
+
2. Library loads subscription from KV store
|
|
389
|
+
3. Library calls `onWebhook(subscriptionId, payload, headers)`
|
|
390
|
+
4. Handler parses payload, returns `WebhookChangeInfo`
|
|
391
|
+
5. Library calls client webhook with notification
|
|
392
|
+
|
|
393
|
+
**Step 3: MCP Server Notifies Client**
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
POST https://client.example.com/webhooks/mcp
|
|
397
|
+
X-MCP-Signature: sha256=...
|
|
398
|
+
Content-Type: application/json
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
"subscriptionId": "sub_xyz789",
|
|
402
|
+
"resourceUri": "github://repo/octocat/hello-world/issues/42",
|
|
403
|
+
"changeType": "created",
|
|
404
|
+
"data": {
|
|
405
|
+
"number": 42,
|
|
406
|
+
"title": "New issue",
|
|
407
|
+
"state": "open"
|
|
408
|
+
},
|
|
409
|
+
"timestamp": 1698765432000
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Step 4: Client Unsubscribes**
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
POST /mcp/resources/unsubscribe
|
|
417
|
+
Authorization: Bearer <token>
|
|
418
|
+
|
|
419
|
+
{
|
|
420
|
+
"method": "resources/unsubscribe",
|
|
421
|
+
"params": {
|
|
422
|
+
"subscriptionId": "sub_xyz789"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Internal Flow:**
|
|
428
|
+
1. Library loads subscription from KV store
|
|
429
|
+
2. Library calls `onUnsubscribe(uri, subscriptionId, storedData, context)`
|
|
430
|
+
3. Handler removes webhook from third-party service
|
|
431
|
+
4. Library deletes subscription from KV store
|
|
432
|
+
|
|
433
|
+
**Reference:**
|
|
434
|
+
- [Subscription Example: GitHub](#)
|
|
435
|
+
- [Subscription Example: Google Drive](#)
|
|
436
|
+
- [Subscription Example: Slack](#)
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 8. Key-Value Store Interface
|
|
441
|
+
|
|
442
|
+
The library requires a persistent store for subscription data.
|
|
443
|
+
|
|
444
|
+
### 8.1 Store Interface
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
interface KeyValueStore {
|
|
448
|
+
/**
|
|
449
|
+
* Get value by key
|
|
450
|
+
* @returns Value as string, or null if not found
|
|
451
|
+
*/
|
|
452
|
+
get(key: string): Promise<string | null>;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Set value with optional TTL
|
|
456
|
+
* @param key - Key to set
|
|
457
|
+
* @param value - Value (will be JSON stringified)
|
|
458
|
+
* @param ttl - Time to live in seconds (optional)
|
|
459
|
+
*/
|
|
460
|
+
set(key: string, value: string, ttl?: number): Promise<void>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Delete key
|
|
464
|
+
*/
|
|
465
|
+
delete(key: string): Promise<void>;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Scan keys by pattern (optional, for debugging)
|
|
469
|
+
* @param pattern - Glob pattern (e.g., "subscription:*")
|
|
470
|
+
*/
|
|
471
|
+
scan?(pattern: string): Promise<string[]>;
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 8.2 Storage Schema
|
|
476
|
+
|
|
477
|
+
The library uses these key patterns:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// Primary subscription data
|
|
481
|
+
`subscription:{subscriptionId}` → {
|
|
482
|
+
uri: string,
|
|
483
|
+
resourceType: string,
|
|
484
|
+
clientCallbackUrl: string,
|
|
485
|
+
clientCallbackSecret?: string,
|
|
486
|
+
userId: string,
|
|
487
|
+
thirdPartyWebhookId: string,
|
|
488
|
+
metadata?: any,
|
|
489
|
+
createdAt: number
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// User subscription index
|
|
493
|
+
`user:{userId}:subscriptions` → string[] // Array of subscriptionIds
|
|
494
|
+
|
|
495
|
+
// Resource subscription index (optional, for bulk operations)
|
|
496
|
+
`resource:{resourceUri}:subscriptions` → string[]
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 8.3 Redis Implementation
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import Redis from 'ioredis';
|
|
503
|
+
|
|
504
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
505
|
+
|
|
506
|
+
const store: KeyValueStore = {
|
|
507
|
+
get: async (key) => await redis.get(key),
|
|
508
|
+
|
|
509
|
+
set: async (key, value, ttl) => {
|
|
510
|
+
if (ttl) {
|
|
511
|
+
await redis.setex(key, ttl, value);
|
|
512
|
+
} else {
|
|
513
|
+
await redis.set(key, value);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
delete: async (key) => {
|
|
518
|
+
await redis.del(key);
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
scan: async (pattern) => {
|
|
522
|
+
const keys: string[] = [];
|
|
523
|
+
let cursor = '0';
|
|
524
|
+
do {
|
|
525
|
+
const [newCursor, matches] = await redis.scan(
|
|
526
|
+
cursor,
|
|
527
|
+
'MATCH',
|
|
528
|
+
pattern,
|
|
529
|
+
'COUNT',
|
|
530
|
+
100
|
|
531
|
+
);
|
|
532
|
+
cursor = newCursor;
|
|
533
|
+
keys.push(...matches);
|
|
534
|
+
} while (cursor !== '0');
|
|
535
|
+
return keys;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Reference:**
|
|
541
|
+
- [Store Examples: Redis](#)
|
|
542
|
+
- [Store Examples: DynamoDB](#)
|
|
543
|
+
- [Store Examples: PostgreSQL](#)
|
|
544
|
+
- [Store Examples: In-Memory (Dev Only)](#)
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## 9. Webhook Configuration
|
|
549
|
+
|
|
550
|
+
### 9.1 Webhook Config Interface
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
interface WebhookConfig {
|
|
554
|
+
// Incoming webhooks from third-parties
|
|
555
|
+
incomingPath?: string; // Default: '/webhooks/incoming'
|
|
556
|
+
incomingSecret?: string; // Shared secret for verification
|
|
557
|
+
verifyIncomingSignature?: (
|
|
558
|
+
payload: any,
|
|
559
|
+
signature: string,
|
|
560
|
+
secret: string
|
|
561
|
+
) => boolean;
|
|
562
|
+
|
|
563
|
+
// Outgoing webhooks to clients
|
|
564
|
+
outgoing?: {
|
|
565
|
+
timeout?: number; // Default: 5000ms
|
|
566
|
+
retries?: number; // Default: 3
|
|
567
|
+
retryDelay?: number; // Default: 1000ms (exponential backoff)
|
|
568
|
+
|
|
569
|
+
// Sign outgoing webhook payloads
|
|
570
|
+
signPayload?: (payload: any, secret: string) => string;
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 9.2 Signature Verification Examples
|
|
576
|
+
|
|
577
|
+
**GitHub Signature Verification:**
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import crypto from 'crypto';
|
|
581
|
+
|
|
582
|
+
function verifyGitHubSignature(
|
|
583
|
+
payload: any,
|
|
584
|
+
signature: string,
|
|
585
|
+
secret: string
|
|
586
|
+
): boolean {
|
|
587
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
588
|
+
hmac.update(JSON.stringify(payload));
|
|
589
|
+
const expected = `sha256=${hmac.digest('hex')}`;
|
|
590
|
+
return crypto.timingSafeEqual(
|
|
591
|
+
Buffer.from(signature),
|
|
592
|
+
Buffer.from(expected)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Slack Signature Verification:**
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
function verifySlackSignature(
|
|
601
|
+
payload: any,
|
|
602
|
+
signature: string,
|
|
603
|
+
timestamp: string,
|
|
604
|
+
secret: string
|
|
605
|
+
): boolean {
|
|
606
|
+
const baseString = `v0:${timestamp}:${JSON.stringify(payload)}`;
|
|
607
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
608
|
+
hmac.update(baseString);
|
|
609
|
+
const expected = `v0=${hmac.digest('hex')}`;
|
|
610
|
+
return crypto.timingSafeEqual(
|
|
611
|
+
Buffer.from(signature),
|
|
612
|
+
Buffer.from(expected)
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Reference:**
|
|
618
|
+
- [Webhook Security Guide](#)
|
|
619
|
+
- [Third-Party Webhook Formats](#)
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## 10. Error Handling
|
|
624
|
+
|
|
625
|
+
### 10.1 Error Types
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
class MCPError extends Error {
|
|
629
|
+
code: number;
|
|
630
|
+
data?: any;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
class AuthenticationError extends MCPError { code = -32001 }
|
|
634
|
+
class ValidationError extends MCPError { code = -32002 }
|
|
635
|
+
class ResourceNotFoundError extends MCPError { code = -32003 }
|
|
636
|
+
class ToolExecutionError extends MCPError { code = -32004 }
|
|
637
|
+
class WebhookError extends MCPError { code = -32005 }
|
|
638
|
+
class StorageError extends MCPError { code = -32006 }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 10.2 Retry Logic
|
|
642
|
+
|
|
643
|
+
The library implements exponential backoff for client webhook calls:
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// Pseudocode
|
|
647
|
+
for (attempt = 0; attempt < maxRetries; attempt++) {
|
|
648
|
+
try {
|
|
649
|
+
response = await callClientWebhook(url, payload);
|
|
650
|
+
if (response.ok) return success;
|
|
651
|
+
|
|
652
|
+
// Don't retry 4xx client errors
|
|
653
|
+
if (response.status >= 400 && response.status < 500) {
|
|
654
|
+
return failure;
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (attempt < maxRetries - 1) {
|
|
658
|
+
await sleep(retryDelay * Math.pow(2, attempt));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// All retries failed - store in dead letter queue
|
|
664
|
+
await storeFailedWebhook(url, payload);
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Reference:**
|
|
668
|
+
- [Error Handling Guide](#)
|
|
669
|
+
- [Dead Letter Queue Setup](#)
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 11. Authentication
|
|
674
|
+
|
|
675
|
+
### 11.1 Authentication Handler
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
interface AuthContext {
|
|
679
|
+
userId: string;
|
|
680
|
+
[key: string]: any; // Additional context (tokens, permissions, etc.)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
type AuthenticateFunction = (req: Request) => Promise<AuthContext>;
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 11.2 Example Implementations
|
|
687
|
+
|
|
688
|
+
**Bearer Token:**
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
authenticate: async (req) => {
|
|
692
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
693
|
+
if (!token) throw new AuthenticationError('Missing token');
|
|
694
|
+
|
|
695
|
+
const payload = await verifyJWT(token);
|
|
696
|
+
return {
|
|
697
|
+
userId: payload.sub,
|
|
698
|
+
email: payload.email,
|
|
699
|
+
githubToken: payload.githubToken
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**API Key:**
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
authenticate: async (req) => {
|
|
708
|
+
const apiKey = req.headers['x-api-key'];
|
|
709
|
+
if (!apiKey) throw new AuthenticationError('Missing API key');
|
|
710
|
+
|
|
711
|
+
const user = await db.users.findByApiKey(apiKey);
|
|
712
|
+
if (!user) throw new AuthenticationError('Invalid API key');
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
userId: user.id,
|
|
716
|
+
permissions: user.permissions
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**OAuth2:**
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
authenticate: async (req) => {
|
|
725
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
726
|
+
const userInfo = await oauth2Client.verifyToken(token);
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
userId: userInfo.sub,
|
|
730
|
+
email: userInfo.email,
|
|
731
|
+
scopes: userInfo.scope.split(' ')
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Reference:**
|
|
737
|
+
- [Authentication Examples](#)
|
|
738
|
+
- [OAuth2 Integration Guide](#)
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## 12. Complete Example: GitHub MCP Server
|
|
743
|
+
|
|
744
|
+
**File:** `github-mcp-server.ts`
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
import { createMCPServer } from 'mcp-http-webhook';
|
|
748
|
+
import { Octokit } from '@octokit/rest';
|
|
749
|
+
import Redis from 'ioredis';
|
|
750
|
+
import crypto from 'crypto';
|
|
751
|
+
|
|
752
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
753
|
+
|
|
754
|
+
const server = createMCPServer({
|
|
755
|
+
name: 'github-mcp',
|
|
756
|
+
version: '1.0.0',
|
|
757
|
+
port: 3000,
|
|
758
|
+
publicUrl: process.env.PUBLIC_URL || 'https://mcp.example.com',
|
|
759
|
+
|
|
760
|
+
store: {
|
|
761
|
+
get: async (key) => await redis.get(key),
|
|
762
|
+
set: async (key, value, ttl) => {
|
|
763
|
+
if (ttl) await redis.setex(key, ttl, value);
|
|
764
|
+
else await redis.set(key, value);
|
|
765
|
+
},
|
|
766
|
+
delete: async (key) => await redis.del(key),
|
|
767
|
+
scan: async (pattern) => {
|
|
768
|
+
const keys: string[] = [];
|
|
769
|
+
let cursor = '0';
|
|
770
|
+
do {
|
|
771
|
+
const [newCursor, matches] = await redis.scan(cursor, 'MATCH', pattern);
|
|
772
|
+
cursor = newCursor;
|
|
773
|
+
keys.push(...matches);
|
|
774
|
+
} while (cursor !== '0');
|
|
775
|
+
return keys;
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
authenticate: async (req) => {
|
|
780
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
781
|
+
if (!token) throw new Error('Unauthorized');
|
|
782
|
+
|
|
783
|
+
const payload = await verifyJWT(token);
|
|
784
|
+
return {
|
|
785
|
+
userId: payload.sub,
|
|
786
|
+
githubToken: payload.githubToken
|
|
787
|
+
};
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
tools: [
|
|
791
|
+
{
|
|
792
|
+
name: 'create_issue',
|
|
793
|
+
description: 'Create a new GitHub issue',
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: 'object',
|
|
796
|
+
properties: {
|
|
797
|
+
owner: { type: 'string', description: 'Repository owner' },
|
|
798
|
+
repo: { type: 'string', description: 'Repository name' },
|
|
799
|
+
title: { type: 'string', description: 'Issue title' },
|
|
800
|
+
body: { type: 'string', description: 'Issue description' }
|
|
801
|
+
},
|
|
802
|
+
required: ['owner', 'repo', 'title']
|
|
803
|
+
},
|
|
804
|
+
handler: async (input, context) => {
|
|
805
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
806
|
+
const { data } = await octokit.issues.create({
|
|
807
|
+
owner: input.owner,
|
|
808
|
+
repo: input.repo,
|
|
809
|
+
title: input.title,
|
|
810
|
+
body: input.body
|
|
811
|
+
});
|
|
812
|
+
return { issue: data };
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: 'list_repositories',
|
|
817
|
+
description: 'List all repositories for authenticated user',
|
|
818
|
+
inputSchema: {
|
|
819
|
+
type: 'object',
|
|
820
|
+
properties: {
|
|
821
|
+
type: {
|
|
822
|
+
type: 'string',
|
|
823
|
+
enum: ['all', 'owner', 'member'],
|
|
824
|
+
default: 'all'
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
handler: async (input, context) => {
|
|
829
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
830
|
+
const { data } = await octokit.repos.listForAuthenticatedUser({
|
|
831
|
+
type: input.type || 'all'
|
|
832
|
+
});
|
|
833
|
+
return { repositories: data };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
],
|
|
837
|
+
|
|
838
|
+
resources: [
|
|
839
|
+
{
|
|
840
|
+
uri: 'github://repo/{owner}/{repo}/issues',
|
|
841
|
+
name: 'GitHub Repository Issues',
|
|
842
|
+
description: 'All issues in a GitHub repository',
|
|
843
|
+
mimeType: 'application/json',
|
|
844
|
+
|
|
845
|
+
read: async (uri, context) => {
|
|
846
|
+
const { owner, repo } = parseUriTemplate(uri);
|
|
847
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
848
|
+
const { data } = await octokit.issues.listForRepo({
|
|
849
|
+
owner,
|
|
850
|
+
repo,
|
|
851
|
+
state: 'all'
|
|
852
|
+
});
|
|
853
|
+
return { contents: data };
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
list: async (context) => {
|
|
857
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
858
|
+
const { data } = await octokit.repos.listForAuthenticatedUser();
|
|
859
|
+
|
|
860
|
+
return data.map(repo => ({
|
|
861
|
+
uri: `github://repo/${repo.owner.login}/${repo.name}/issues`,
|
|
862
|
+
name: `${repo.full_name} Issues`,
|
|
863
|
+
description: `Issue tracker for ${repo.full_name}`,
|
|
864
|
+
mimeType: 'application/json'
|
|
865
|
+
}));
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
subscription: {
|
|
869
|
+
onSubscribe: async (uri, subscriptionId, thirdPartyWebhookUrl, context) => {
|
|
870
|
+
const { owner, repo } = parseUriTemplate(uri);
|
|
871
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
872
|
+
|
|
873
|
+
// Create webhook in GitHub pointing to our server
|
|
874
|
+
const { data: webhook } = await octokit.repos.createWebhook({
|
|
875
|
+
owner,
|
|
876
|
+
repo,
|
|
877
|
+
config: {
|
|
878
|
+
url: thirdPartyWebhookUrl,
|
|
879
|
+
content_type: 'json',
|
|
880
|
+
secret: process.env.GITHUB_WEBHOOK_SECRET
|
|
881
|
+
},
|
|
882
|
+
events: ['issues', 'issue_comment']
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
console.log(`Created GitHub webhook ${webhook.id} -> ${thirdPartyWebhookUrl}`);
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
thirdPartyWebhookId: webhook.id.toString(),
|
|
889
|
+
metadata: { owner, repo }
|
|
890
|
+
};
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
onUnsubscribe: async (uri, subscriptionId, storedData, context) => {
|
|
894
|
+
const { owner, repo } = storedData.metadata;
|
|
895
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
896
|
+
|
|
897
|
+
await octokit.repos.deleteWebhook({
|
|
898
|
+
owner,
|
|
899
|
+
repo,
|
|
900
|
+
hook_id: parseInt(storedData.thirdPartyWebhookId)
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
console.log(`Deleted GitHub webhook ${storedData.thirdPartyWebhookId}`);
|
|
904
|
+
},
|
|
905
|
+
|
|
906
|
+
onWebhook: async (subscriptionId, payload, headers) => {
|
|
907
|
+
// Verify GitHub signature
|
|
908
|
+
const signature = headers['x-hub-signature-256'];
|
|
909
|
+
const body = JSON.stringify(payload);
|
|
910
|
+
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!);
|
|
911
|
+
hmac.update(body);
|
|
912
|
+
const expected = `sha256=${hmac.digest('hex')}`;
|
|
913
|
+
|
|
914
|
+
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
915
|
+
throw new Error('Invalid webhook signature');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const event = headers['x-github-event'];
|
|
919
|
+
|
|
920
|
+
if (event === 'issues') {
|
|
921
|
+
const { action, issue, repository } = payload;
|
|
922
|
+
|
|
923
|
+
if (['opened', 'edited', 'closed', 'reopened'].includes(action)) {
|
|
924
|
+
return {
|
|
925
|
+
resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues`,
|
|
926
|
+
changeType: action === 'opened' ? 'created' :
|
|
927
|
+
action === 'closed' ? 'deleted' : 'updated',
|
|
928
|
+
data: {
|
|
929
|
+
issueNumber: issue.number,
|
|
930
|
+
title: issue.title,
|
|
931
|
+
state: issue.state,
|
|
932
|
+
action
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (event === 'issue_comment') {
|
|
939
|
+
const { action, issue, comment, repository } = payload;
|
|
940
|
+
|
|
941
|
+
if (action === 'created') {
|
|
942
|
+
return {
|
|
943
|
+
resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues`,
|
|
944
|
+
changeType: 'updated',
|
|
945
|
+
data: {
|
|
946
|
+
issueNumber: issue.number,
|
|
947
|
+
commentId: comment.id,
|
|
948
|
+
commentBody: comment.body,
|
|
949
|
+
commentAuthor: comment.user.login
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
],
|
|
960
|
+
|
|
961
|
+
webhooks: {
|
|
962
|
+
incomingPath: '/webhooks/incoming',
|
|
963
|
+
incomingSecret: process.env.GITHUB_WEBHOOK_SECRET,
|
|
964
|
+
verifyIncomingSignature: (payload, signature, secret) => {
|
|
965
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
966
|
+
hmac.update(JSON.stringify(payload));
|
|
967
|
+
const expected = `sha256=${hmac.digest('hex')}`;
|
|
968
|
+
return crypto.timingSafeEqual(
|
|
969
|
+
Buffer.from(signature),
|
|
970
|
+
Buffer.from(expected)
|
|
971
|
+
);
|
|
972
|
+
},
|
|
973
|
+
outgoing: {
|
|
974
|
+
timeout: 5000,
|
|
975
|
+
retries: 3,
|
|
976
|
+
retryDelay: 1000,
|
|
977
|
+
signPayload: (payload, secret) => {
|
|
978
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
979
|
+
hmac.update(JSON.stringify(payload));
|
|
980
|
+
return `sha256=${hmac.digest('hex')}`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
logger: console,
|
|
986
|
+
logLevel: 'info'
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
await server.start();
|
|
990
|
+
console.log(`GitHub MCP server running on port 3000`);
|
|
991
|
+
console.log(`Webhook endpoint: ${process.env.PUBLIC_URL}/webhooks/incoming/{subscriptionId}`);
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
**Reference:**
|
|
995
|
+
- [Complete Examples Repository](#)
|
|
996
|
+
- [GitHub Integration Guide](#)
|
|
997
|
+
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
## 13. Deployment Guide
|
|
1001
|
+
|
|
1002
|
+
### 13.1 Environment Variables
|
|
1003
|
+
|
|
1004
|
+
```bash
|
|
1005
|
+
# Server Configuration
|
|
1006
|
+
PORT=3000
|
|
1007
|
+
PUBLIC_URL=https://mcp.example.com
|
|
1008
|
+
NODE_ENV=production
|
|
1009
|
+
|
|
1010
|
+
# Redis Configuration
|
|
1011
|
+
REDIS_URL=redis://localhost:6379
|
|
1012
|
+
|
|
1013
|
+
# GitHub Configuration
|
|
1014
|
+
GITHUB_WEBHOOK_SECRET=your-webhook-secret
|
|
1015
|
+
|
|
1016
|
+
# JWT Configuration
|
|
1017
|
+
JWT_SECRET=your-jwt-secret
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### 13.2 Docker Deployment
|
|
1021
|
+
|
|
1022
|
+
**Dockerfile:**
|
|
1023
|
+
|
|
1024
|
+
```dockerfile
|
|
1025
|
+
FROM node:20-alpine
|
|
1026
|
+
|
|
1027
|
+
WORKDIR /app
|
|
1028
|
+
|
|
1029
|
+
COPY package*.json ./
|
|
1030
|
+
RUN npm ci --production
|
|
1031
|
+
|
|
1032
|
+
COPY . .
|
|
1033
|
+
RUN npm run build
|
|
1034
|
+
|
|
1035
|
+
EXPOSE 3000
|
|
1036
|
+
|
|
1037
|
+
CMD ["node", "dist/index.js"]
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
**docker-compose.yml:**
|
|
1041
|
+
|
|
1042
|
+
```yaml
|
|
1043
|
+
version: '3.8'
|
|
1044
|
+
|
|
1045
|
+
services:
|
|
1046
|
+
mcp-server:
|
|
1047
|
+
build: .
|
|
1048
|
+
ports:
|
|
1049
|
+
- "3000:3000"
|
|
1050
|
+
environment:
|
|
1051
|
+
- PUBLIC_URL=https://mcp.example.com
|
|
1052
|
+
- REDIS_URL=redis://redis:6379
|
|
1053
|
+
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
|
|
1054
|
+
depends_on:
|
|
1055
|
+
- redis
|
|
1056
|
+
|
|
1057
|
+
redis:
|
|
1058
|
+
image: redis:7-alpine
|
|
1059
|
+
volumes:
|
|
1060
|
+
- redis-data:/data
|
|
1061
|
+
|
|
1062
|
+
volumes:
|
|
1063
|
+
redis-data:
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### 13.3 Kubernetes Deployment
|
|
1067
|
+
|
|
1068
|
+
**deployment.yaml:**
|
|
1069
|
+
|
|
1070
|
+
```yaml
|
|
1071
|
+
apiVersion: apps/v1
|
|
1072
|
+
kind: Deployment
|
|
1073
|
+
metadata:
|
|
1074
|
+
name: mcp-http-webhook
|
|
1075
|
+
labels:
|
|
1076
|
+
app: mcp-http-webhook
|
|
1077
|
+
spec:
|
|
1078
|
+
replicas: 3
|
|
1079
|
+
selector:
|
|
1080
|
+
matchLabels:
|
|
1081
|
+
app: mcp-http-webhook
|
|
1082
|
+
template:
|
|
1083
|
+
metadata:
|
|
1084
|
+
labels:
|
|
1085
|
+
app: mcp-http-webhook
|
|
1086
|
+
spec:
|
|
1087
|
+
containers:
|
|
1088
|
+
- name: mcp-server
|
|
1089
|
+
image: your-registry/mcp-http-webhook:latest
|
|
1090
|
+
ports:
|
|
1091
|
+
- containerPort: 3000
|
|
1092
|
+
env:
|
|
1093
|
+
- name: PORT
|
|
1094
|
+
value: "3000"
|
|
1095
|
+
- name: PUBLIC_URL
|
|
1096
|
+
value: "https://mcp.example.com"
|
|
1097
|
+
- name: REDIS_URL
|
|
1098
|
+
valueFrom:
|
|
1099
|
+
secretKeyRef:
|
|
1100
|
+
name: mcp-secrets
|
|
1101
|
+
key: redis-url
|
|
1102
|
+
- name: GITHUB_WEBHOOK_SECRET
|
|
1103
|
+
valueFrom:
|
|
1104
|
+
secretKeyRef:
|
|
1105
|
+
name: mcp-secrets
|
|
1106
|
+
key: github-webhook-secret
|
|
1107
|
+
- name: JWT_SECRET
|
|
1108
|
+
valueFrom:
|
|
1109
|
+
secretKeyRef:
|
|
1110
|
+
name: mcp-secrets
|
|
1111
|
+
key: jwt-secret
|
|
1112
|
+
resources:
|
|
1113
|
+
requests:
|
|
1114
|
+
memory: "256Mi"
|
|
1115
|
+
cpu: "250m"
|
|
1116
|
+
limits:
|
|
1117
|
+
memory: "512Mi"
|
|
1118
|
+
cpu: "500m"
|
|
1119
|
+
livenessProbe:
|
|
1120
|
+
httpGet:
|
|
1121
|
+
path: /health
|
|
1122
|
+
port: 3000
|
|
1123
|
+
initialDelaySeconds: 30
|
|
1124
|
+
periodSeconds: 10
|
|
1125
|
+
readinessProbe:
|
|
1126
|
+
httpGet:
|
|
1127
|
+
path: /ready
|
|
1128
|
+
port: 3000
|
|
1129
|
+
initialDelaySeconds: 5
|
|
1130
|
+
periodSeconds: 5
|
|
1131
|
+
|
|
1132
|
+
---
|
|
1133
|
+
apiVersion: v1
|
|
1134
|
+
kind: Service
|
|
1135
|
+
metadata:
|
|
1136
|
+
name: mcp-http-webhook
|
|
1137
|
+
spec:
|
|
1138
|
+
selector:
|
|
1139
|
+
app: mcp-http-webhook
|
|
1140
|
+
ports:
|
|
1141
|
+
- protocol: TCP
|
|
1142
|
+
port: 80
|
|
1143
|
+
targetPort: 3000
|
|
1144
|
+
type: ClusterIP
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
apiVersion: networking.k8s.io/v1
|
|
1148
|
+
kind: Ingress
|
|
1149
|
+
metadata:
|
|
1150
|
+
name: mcp-http-webhook
|
|
1151
|
+
annotations:
|
|
1152
|
+
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
1153
|
+
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
1154
|
+
spec:
|
|
1155
|
+
ingressClassName: nginx
|
|
1156
|
+
tls:
|
|
1157
|
+
- hosts:
|
|
1158
|
+
- mcp.example.com
|
|
1159
|
+
secretName: mcp-tls
|
|
1160
|
+
rules:
|
|
1161
|
+
- host: mcp.example.com
|
|
1162
|
+
http:
|
|
1163
|
+
paths:
|
|
1164
|
+
- path: /
|
|
1165
|
+
pathType: Prefix
|
|
1166
|
+
backend:
|
|
1167
|
+
service:
|
|
1168
|
+
name: mcp-http-webhook
|
|
1169
|
+
port:
|
|
1170
|
+
number: 80
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
**secrets.yaml:**
|
|
1174
|
+
|
|
1175
|
+
```yaml
|
|
1176
|
+
apiVersion: v1
|
|
1177
|
+
kind: Secret
|
|
1178
|
+
metadata:
|
|
1179
|
+
name: mcp-secrets
|
|
1180
|
+
type: Opaque
|
|
1181
|
+
stringData:
|
|
1182
|
+
redis-url: "redis://redis-service:6379"
|
|
1183
|
+
github-webhook-secret: "your-webhook-secret"
|
|
1184
|
+
jwt-secret: "your-jwt-secret"
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
### 13.4 Scaling Considerations
|
|
1188
|
+
|
|
1189
|
+
**Horizontal Scaling:**
|
|
1190
|
+
- Multiple instances can run simultaneously
|
|
1191
|
+
- No connection state = trivial load balancing
|
|
1192
|
+
- All state stored in Redis/external KV store
|
|
1193
|
+
- Use sticky sessions NOT required
|
|
1194
|
+
|
|
1195
|
+
**Load Balancer Configuration:**
|
|
1196
|
+
```nginx
|
|
1197
|
+
upstream mcp_backend {
|
|
1198
|
+
least_conn; # Or round_robin
|
|
1199
|
+
server mcp-1:3000;
|
|
1200
|
+
server mcp-2:3000;
|
|
1201
|
+
server mcp-3:3000;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
server {
|
|
1205
|
+
listen 443 ssl;
|
|
1206
|
+
server_name mcp.example.com;
|
|
1207
|
+
|
|
1208
|
+
location / {
|
|
1209
|
+
proxy_pass http://mcp_backend;
|
|
1210
|
+
proxy_set_header Host $host;
|
|
1211
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
1212
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1213
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
**Reference:**
|
|
1219
|
+
- [Deployment Examples](#)
|
|
1220
|
+
- [Kubernetes Best Practices](#)
|
|
1221
|
+
- [High Availability Guide](#)
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## 14. Monitoring & Observability
|
|
1226
|
+
|
|
1227
|
+
### 14.1 Health Check Endpoints
|
|
1228
|
+
|
|
1229
|
+
The library automatically exposes:
|
|
1230
|
+
|
|
1231
|
+
```
|
|
1232
|
+
GET /health # Basic health check
|
|
1233
|
+
GET /ready # Readiness check (includes store connectivity)
|
|
1234
|
+
GET /metrics # Prometheus metrics (optional)
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
### 14.2 Logging
|
|
1238
|
+
|
|
1239
|
+
**Structured Logging Interface:**
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
interface Logger {
|
|
1243
|
+
debug(message: string, meta?: any): void;
|
|
1244
|
+
info(message: string, meta?: any): void;
|
|
1245
|
+
warn(message: string, meta?: any): void;
|
|
1246
|
+
error(message: string, meta?: any): void;
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
**Winston Example:**
|
|
1251
|
+
|
|
1252
|
+
```typescript
|
|
1253
|
+
import winston from 'winston';
|
|
1254
|
+
|
|
1255
|
+
const logger = winston.createLogger({
|
|
1256
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
1257
|
+
format: winston.format.json(),
|
|
1258
|
+
transports: [
|
|
1259
|
+
new winston.transports.Console({
|
|
1260
|
+
format: winston.format.combine(
|
|
1261
|
+
winston.format.timestamp(),
|
|
1262
|
+
winston.format.json()
|
|
1263
|
+
)
|
|
1264
|
+
})
|
|
1265
|
+
]
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
const server = createMCPServer({
|
|
1269
|
+
// ...
|
|
1270
|
+
logger,
|
|
1271
|
+
logLevel: 'info'
|
|
1272
|
+
});
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### 14.3 Metrics
|
|
1276
|
+
|
|
1277
|
+
**Key Metrics Tracked:**
|
|
1278
|
+
|
|
1279
|
+
- `mcp_requests_total` - Total HTTP requests (by method, status)
|
|
1280
|
+
- `mcp_request_duration_seconds` - Request latency histogram
|
|
1281
|
+
- `mcp_tool_calls_total` - Tool invocations (by tool name, status)
|
|
1282
|
+
- `mcp_resource_reads_total` - Resource read operations
|
|
1283
|
+
- `mcp_subscriptions_active` - Current active subscriptions
|
|
1284
|
+
- `mcp_webhook_incoming_total` - Third-party webhooks received
|
|
1285
|
+
- `mcp_webhook_outgoing_total` - Client notifications sent (by status)
|
|
1286
|
+
- `mcp_webhook_retry_total` - Webhook retry attempts
|
|
1287
|
+
- `mcp_store_operations_total` - KV store operations (by type)
|
|
1288
|
+
|
|
1289
|
+
**Prometheus Integration:**
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
import promClient from 'prom-client';
|
|
1293
|
+
|
|
1294
|
+
const register = new promClient.Registry();
|
|
1295
|
+
promClient.collectDefaultMetrics({ register });
|
|
1296
|
+
|
|
1297
|
+
const server = createMCPServer({
|
|
1298
|
+
// ...
|
|
1299
|
+
metrics: {
|
|
1300
|
+
enabled: true,
|
|
1301
|
+
registry: register
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Metrics available at GET /metrics
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
### 14.4 Distributed Tracing
|
|
1309
|
+
|
|
1310
|
+
**OpenTelemetry Integration:**
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
import { trace } from '@opentelemetry/api';
|
|
1314
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
1315
|
+
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
|
|
1316
|
+
|
|
1317
|
+
const provider = new NodeTracerProvider();
|
|
1318
|
+
provider.addSpanProcessor(
|
|
1319
|
+
new SimpleSpanProcessor(new JaegerExporter())
|
|
1320
|
+
);
|
|
1321
|
+
provider.register();
|
|
1322
|
+
|
|
1323
|
+
const server = createMCPServer({
|
|
1324
|
+
// ...
|
|
1325
|
+
tracing: {
|
|
1326
|
+
enabled: true,
|
|
1327
|
+
serviceName: 'mcp-http-webhook'
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
**Reference:**
|
|
1333
|
+
- [Monitoring Setup Guide](#)
|
|
1334
|
+
- [Grafana Dashboard Templates](#)
|
|
1335
|
+
- [Alert Configuration Examples](#)
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
## 15. Testing
|
|
1340
|
+
|
|
1341
|
+
### 15.1 Unit Testing
|
|
1342
|
+
|
|
1343
|
+
**Example using Jest:**
|
|
1344
|
+
|
|
1345
|
+
```typescript
|
|
1346
|
+
import { describe, test, expect, beforeEach } from '@jest/globals';
|
|
1347
|
+
import { createMCPServer } from 'mcp-http-webhook';
|
|
1348
|
+
|
|
1349
|
+
describe('MCP Server', () => {
|
|
1350
|
+
let server: MCPServer;
|
|
1351
|
+
let mockStore: KeyValueStore;
|
|
1352
|
+
|
|
1353
|
+
beforeEach(() => {
|
|
1354
|
+
mockStore = {
|
|
1355
|
+
get: jest.fn(),
|
|
1356
|
+
set: jest.fn(),
|
|
1357
|
+
delete: jest.fn()
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
server = createMCPServer({
|
|
1361
|
+
name: 'test-server',
|
|
1362
|
+
version: '1.0.0',
|
|
1363
|
+
publicUrl: 'http://localhost:3000',
|
|
1364
|
+
store: mockStore,
|
|
1365
|
+
tools: [],
|
|
1366
|
+
resources: []
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
test('should create server instance', () => {
|
|
1371
|
+
expect(server).toBeDefined();
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test('should handle tool calls', async () => {
|
|
1375
|
+
// Test implementation
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
### 15.2 Integration Testing
|
|
1381
|
+
|
|
1382
|
+
**Testing Subscription Flow:**
|
|
1383
|
+
|
|
1384
|
+
```typescript
|
|
1385
|
+
import request from 'supertest';
|
|
1386
|
+
|
|
1387
|
+
describe('Subscription Flow', () => {
|
|
1388
|
+
test('should create subscription and handle webhook', async () => {
|
|
1389
|
+
// 1. Subscribe to resource
|
|
1390
|
+
const subscribeRes = await request(app)
|
|
1391
|
+
.post('/mcp/resources/subscribe')
|
|
1392
|
+
.set('Authorization', 'Bearer test-token')
|
|
1393
|
+
.send({
|
|
1394
|
+
method: 'resources/subscribe',
|
|
1395
|
+
params: {
|
|
1396
|
+
uri: 'github://repo/test/test/issues',
|
|
1397
|
+
callbackUrl: 'http://client.test/webhook',
|
|
1398
|
+
callbackSecret: 'test-secret'
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
expect(subscribeRes.status).toBe(200);
|
|
1403
|
+
const { subscriptionId } = subscribeRes.body;
|
|
1404
|
+
|
|
1405
|
+
// 2. Simulate third-party webhook
|
|
1406
|
+
const webhookRes = await request(app)
|
|
1407
|
+
.post(`/webhooks/incoming/${subscriptionId}`)
|
|
1408
|
+
.set('X-GitHub-Event', 'issues')
|
|
1409
|
+
.send({
|
|
1410
|
+
action: 'opened',
|
|
1411
|
+
issue: { number: 1, title: 'Test' },
|
|
1412
|
+
repository: { owner: { login: 'test' }, name: 'test' }
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
expect(webhookRes.status).toBe(200);
|
|
1416
|
+
|
|
1417
|
+
// 3. Verify client webhook was called
|
|
1418
|
+
// (Use nock or similar to mock HTTP calls)
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
### 15.3 Load Testing
|
|
1424
|
+
|
|
1425
|
+
**Using k6:**
|
|
1426
|
+
|
|
1427
|
+
```javascript
|
|
1428
|
+
import http from 'k6/http';
|
|
1429
|
+
import { check, sleep } from 'k6';
|
|
1430
|
+
|
|
1431
|
+
export let options = {
|
|
1432
|
+
stages: [
|
|
1433
|
+
{ duration: '30s', target: 20 },
|
|
1434
|
+
{ duration: '1m', target: 50 },
|
|
1435
|
+
{ duration: '30s', target: 0 }
|
|
1436
|
+
]
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
export default function() {
|
|
1440
|
+
const payload = JSON.stringify({
|
|
1441
|
+
method: 'tools/call',
|
|
1442
|
+
params: {
|
|
1443
|
+
name: 'create_issue',
|
|
1444
|
+
arguments: {
|
|
1445
|
+
owner: 'test',
|
|
1446
|
+
repo: 'test',
|
|
1447
|
+
title: 'Load test issue'
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const params = {
|
|
1453
|
+
headers: {
|
|
1454
|
+
'Content-Type': 'application/json',
|
|
1455
|
+
'Authorization': 'Bearer test-token'
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
const res = http.post('http://localhost:3000/mcp/tools/call', payload, params);
|
|
1460
|
+
|
|
1461
|
+
check(res, {
|
|
1462
|
+
'status is 200': (r) => r.status === 200,
|
|
1463
|
+
'response time < 500ms': (r) => r.timings.duration < 500
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
sleep(1);
|
|
1467
|
+
}
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
**Reference:**
|
|
1471
|
+
- [Testing Examples](#)
|
|
1472
|
+
- [Mock Store Implementation](#)
|
|
1473
|
+
- [Load Testing Guide](#)
|
|
1474
|
+
|
|
1475
|
+
---
|
|
1476
|
+
|
|
1477
|
+
## 16. Security Considerations
|
|
1478
|
+
|
|
1479
|
+
### 16.1 Webhook Security
|
|
1480
|
+
|
|
1481
|
+
**Critical Security Practices:**
|
|
1482
|
+
|
|
1483
|
+
1. **Always Verify Signatures**
|
|
1484
|
+
- Third-party webhooks: Verify using provider's signature
|
|
1485
|
+
- Client webhooks: Sign outgoing payloads
|
|
1486
|
+
|
|
1487
|
+
2. **Use HTTPS Only**
|
|
1488
|
+
- `publicUrl` must be HTTPS in production
|
|
1489
|
+
- Reject non-HTTPS callback URLs
|
|
1490
|
+
|
|
1491
|
+
3. **Rate Limiting**
|
|
1492
|
+
- Implement rate limits on webhook endpoints
|
|
1493
|
+
- Prevent DoS attacks
|
|
1494
|
+
|
|
1495
|
+
4. **Timeout Configuration**
|
|
1496
|
+
- Set reasonable timeouts for outgoing webhooks
|
|
1497
|
+
- Prevent hanging connections
|
|
1498
|
+
|
|
1499
|
+
### 16.2 Authentication Best Practices
|
|
1500
|
+
|
|
1501
|
+
```typescript
|
|
1502
|
+
// Example: Multi-factor auth check
|
|
1503
|
+
authenticate: async (req) => {
|
|
1504
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
1505
|
+
const payload = await verifyJWT(token);
|
|
1506
|
+
|
|
1507
|
+
// Verify IP whitelist for sensitive operations
|
|
1508
|
+
if (payload.requiresIpCheck) {
|
|
1509
|
+
const clientIp = req.ip;
|
|
1510
|
+
if (!isIpWhitelisted(clientIp, payload.userId)) {
|
|
1511
|
+
throw new AuthenticationError('IP not whitelisted');
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Check for revoked tokens
|
|
1516
|
+
const isRevoked = await redis.get(`revoked:${payload.jti}`);
|
|
1517
|
+
if (isRevoked) {
|
|
1518
|
+
throw new AuthenticationError('Token revoked');
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return {
|
|
1522
|
+
userId: payload.sub,
|
|
1523
|
+
permissions: payload.permissions
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
### 16.3 Input Validation
|
|
1529
|
+
|
|
1530
|
+
The library automatically validates:
|
|
1531
|
+
- Tool inputs against `inputSchema`
|
|
1532
|
+
- Resource URIs against URI templates
|
|
1533
|
+
- Webhook payloads (basic structure)
|
|
1534
|
+
|
|
1535
|
+
**Additional Validation:**
|
|
1536
|
+
|
|
1537
|
+
```typescript
|
|
1538
|
+
tools: [{
|
|
1539
|
+
name: 'create_issue',
|
|
1540
|
+
inputSchema: {
|
|
1541
|
+
type: 'object',
|
|
1542
|
+
properties: {
|
|
1543
|
+
title: {
|
|
1544
|
+
type: 'string',
|
|
1545
|
+
minLength: 1,
|
|
1546
|
+
maxLength: 256
|
|
1547
|
+
},
|
|
1548
|
+
body: {
|
|
1549
|
+
type: 'string',
|
|
1550
|
+
maxLength: 65536
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
required: ['title']
|
|
1554
|
+
},
|
|
1555
|
+
handler: async (input, context) => {
|
|
1556
|
+
// Additional business logic validation
|
|
1557
|
+
if (containsSpam(input.title)) {
|
|
1558
|
+
throw new ValidationError('Content contains spam');
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Proceed with tool execution
|
|
1562
|
+
}
|
|
1563
|
+
}]
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
### 16.4 Secret Management
|
|
1567
|
+
|
|
1568
|
+
**DO NOT hardcode secrets:**
|
|
1569
|
+
|
|
1570
|
+
```typescript
|
|
1571
|
+
// ❌ BAD
|
|
1572
|
+
const secret = 'my-secret-key';
|
|
1573
|
+
|
|
1574
|
+
// ✅ GOOD
|
|
1575
|
+
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
|
1576
|
+
if (!secret) {
|
|
1577
|
+
throw new Error('GITHUB_WEBHOOK_SECRET not configured');
|
|
1578
|
+
}
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
**Use Secret Management Services:**
|
|
1582
|
+
- AWS Secrets Manager
|
|
1583
|
+
- HashiCorp Vault
|
|
1584
|
+
- Kubernetes Secrets
|
|
1585
|
+
- Azure Key Vault
|
|
1586
|
+
|
|
1587
|
+
**Reference:**
|
|
1588
|
+
- [Security Checklist](#)
|
|
1589
|
+
- [Secret Management Guide](#)
|
|
1590
|
+
- [Penetration Testing Guide](#)
|
|
1591
|
+
|
|
1592
|
+
---
|
|
1593
|
+
|
|
1594
|
+
## 17. Advanced Features
|
|
1595
|
+
|
|
1596
|
+
### 17.1 Batch Operations
|
|
1597
|
+
|
|
1598
|
+
```typescript
|
|
1599
|
+
interface BatchConfig {
|
|
1600
|
+
maxBatchSize?: number;
|
|
1601
|
+
batchTimeout?: number;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const server = createMCPServer({
|
|
1605
|
+
// ...
|
|
1606
|
+
batch: {
|
|
1607
|
+
maxBatchSize: 100,
|
|
1608
|
+
batchTimeout: 1000
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Clients can send multiple requests in one HTTP call
|
|
1613
|
+
POST /mcp/batch
|
|
1614
|
+
{
|
|
1615
|
+
"requests": [
|
|
1616
|
+
{ "method": "tools/call", "params": {...} },
|
|
1617
|
+
{ "method": "resources/read", "params": {...} }
|
|
1618
|
+
]
|
|
1619
|
+
}
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
### 17.2 Caching Layer
|
|
1623
|
+
|
|
1624
|
+
```typescript
|
|
1625
|
+
interface CacheConfig {
|
|
1626
|
+
enabled: boolean;
|
|
1627
|
+
ttl?: number; // Default TTL in seconds
|
|
1628
|
+
keyPrefix?: string;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const server = createMCPServer({
|
|
1632
|
+
// ...
|
|
1633
|
+
cache: {
|
|
1634
|
+
enabled: true,
|
|
1635
|
+
ttl: 300, // 5 minutes
|
|
1636
|
+
keyPrefix: 'mcp:cache:'
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Resources can specify custom cache behavior
|
|
1641
|
+
resources: [{
|
|
1642
|
+
uri: 'github://repo/{owner}/{repo}/issues',
|
|
1643
|
+
cache: {
|
|
1644
|
+
enabled: true,
|
|
1645
|
+
ttl: 600, // 10 minutes
|
|
1646
|
+
key: (uri) => `issues:${uri}`
|
|
1647
|
+
},
|
|
1648
|
+
read: async (uri, context) => {
|
|
1649
|
+
// Will be cached automatically
|
|
1650
|
+
}
|
|
1651
|
+
}]
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
### 17.3 Webhook Dead Letter Queue
|
|
1655
|
+
|
|
1656
|
+
```typescript
|
|
1657
|
+
interface DeadLetterQueueConfig {
|
|
1658
|
+
enabled: boolean;
|
|
1659
|
+
store: KeyValueStore;
|
|
1660
|
+
retention?: number; // Days to retain failed webhooks
|
|
1661
|
+
maxRetries?: number;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const server = createMCPServer({
|
|
1665
|
+
// ...
|
|
1666
|
+
webhooks: {
|
|
1667
|
+
deadLetterQueue: {
|
|
1668
|
+
enabled: true,
|
|
1669
|
+
store: dlqStore,
|
|
1670
|
+
retention: 7,
|
|
1671
|
+
maxRetries: 5
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
// Failed webhooks stored as:
|
|
1677
|
+
// dlq:{timestamp}:{subscriptionId} -> { url, payload, attempts, lastError }
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
### 17.4 Resource Pagination
|
|
1681
|
+
|
|
1682
|
+
```typescript
|
|
1683
|
+
resources: [{
|
|
1684
|
+
uri: 'github://repo/{owner}/{repo}/issues',
|
|
1685
|
+
read: async (uri, context, options) => {
|
|
1686
|
+
const { page = 1, limit = 50 } = options?.pagination || {};
|
|
1687
|
+
|
|
1688
|
+
const octokit = new Octokit({ auth: context.githubToken });
|
|
1689
|
+
const { data } = await octokit.issues.listForRepo({
|
|
1690
|
+
owner,
|
|
1691
|
+
repo,
|
|
1692
|
+
page,
|
|
1693
|
+
per_page: limit
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
return {
|
|
1697
|
+
contents: data,
|
|
1698
|
+
pagination: {
|
|
1699
|
+
page,
|
|
1700
|
+
limit,
|
|
1701
|
+
hasMore: data.length === limit,
|
|
1702
|
+
nextPage: data.length === limit ? page + 1 : null
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
}]
|
|
1707
|
+
|
|
1708
|
+
// Client request:
|
|
1709
|
+
POST /mcp/resources/read
|
|
1710
|
+
{
|
|
1711
|
+
"method": "resources/read",
|
|
1712
|
+
"params": {
|
|
1713
|
+
"uri": "github://repo/test/test/issues",
|
|
1714
|
+
"pagination": {
|
|
1715
|
+
"page": 1,
|
|
1716
|
+
"limit": 50
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
### 17.5 Middleware Support
|
|
1723
|
+
|
|
1724
|
+
```typescript
|
|
1725
|
+
interface Middleware {
|
|
1726
|
+
(req: Request, res: Response, next: NextFunction): void | Promise<void>;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const server = createMCPServer({
|
|
1730
|
+
// ...
|
|
1731
|
+
middleware: [
|
|
1732
|
+
// Rate limiting
|
|
1733
|
+
rateLimit({
|
|
1734
|
+
windowMs: 15 * 60 * 1000,
|
|
1735
|
+
max: 100,
|
|
1736
|
+
message: 'Too many requests'
|
|
1737
|
+
}),
|
|
1738
|
+
|
|
1739
|
+
// Request logging
|
|
1740
|
+
(req, res, next) => {
|
|
1741
|
+
console.log(`${req.method} ${req.path}`);
|
|
1742
|
+
next();
|
|
1743
|
+
},
|
|
1744
|
+
|
|
1745
|
+
// Custom auth middleware
|
|
1746
|
+
async (req, res, next) => {
|
|
1747
|
+
if (req.path.startsWith('/admin')) {
|
|
1748
|
+
// Additional admin checks
|
|
1749
|
+
}
|
|
1750
|
+
next();
|
|
1751
|
+
}
|
|
1752
|
+
]
|
|
1753
|
+
});
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
**Reference:**
|
|
1757
|
+
- [Advanced Features Guide](#)
|
|
1758
|
+
- [Caching Strategies](#)
|
|
1759
|
+
- [DLQ Management](#)
|
|
1760
|
+
|
|
1761
|
+
---
|
|
1762
|
+
|
|
1763
|
+
## 18. Migration Guide
|
|
1764
|
+
|
|
1765
|
+
### 18.1 From Standard MCP SSE Transport
|
|
1766
|
+
|
|
1767
|
+
**Key Changes:**
|
|
1768
|
+
|
|
1769
|
+
| Standard MCP | HTTP Webhook MCP |
|
|
1770
|
+
|--------------|------------------|
|
|
1771
|
+
| Persistent SSE connection | Stateless HTTP requests |
|
|
1772
|
+
| Server pushes to client | Client provides callback URL |
|
|
1773
|
+
| `subscribe()` keeps connection | `subscribe()` returns subscriptionId |
|
|
1774
|
+
| Real-time push | Webhook callback |
|
|
1775
|
+
| Connection per client | Shared webhook endpoints |
|
|
1776
|
+
|
|
1777
|
+
**Migration Steps:**
|
|
1778
|
+
|
|
1779
|
+
1. **Update Client Implementation**
|
|
1780
|
+
- Remove SSE connection logic
|
|
1781
|
+
- Implement webhook receiver endpoint
|
|
1782
|
+
- Provide callback URL in subscribe requests
|
|
1783
|
+
- Handle notifications via webhooks
|
|
1784
|
+
|
|
1785
|
+
2. **Update Server Implementation**
|
|
1786
|
+
- Replace `StdioServerTransport` with `createMCPServer()`
|
|
1787
|
+
- Add KV store configuration
|
|
1788
|
+
- Implement subscription handlers
|
|
1789
|
+
- Configure webhook endpoints
|
|
1790
|
+
|
|
1791
|
+
3. **Update Resource Subscriptions**
|
|
1792
|
+
```typescript
|
|
1793
|
+
// Before (Standard MCP)
|
|
1794
|
+
server.setRequestHandler(SubscribeRequest, async (request) => {
|
|
1795
|
+
// Setup SSE push
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// After (HTTP Webhook)
|
|
1799
|
+
resources: [{
|
|
1800
|
+
subscription: {
|
|
1801
|
+
onSubscribe: async (uri, subscriptionId, webhookUrl, context) => {
|
|
1802
|
+
// Register with third-party
|
|
1803
|
+
// Return metadata
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}]
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
### 18.2 From Custom Implementation
|
|
1810
|
+
|
|
1811
|
+
If migrating from a custom webhook implementation:
|
|
1812
|
+
|
|
1813
|
+
1. **Adopt Standard MCP Protocol**
|
|
1814
|
+
- Use MCP JSON-RPC message format
|
|
1815
|
+
- Implement standard endpoints (`/tools/list`, `/resources/read`, etc.)
|
|
1816
|
+
|
|
1817
|
+
2. **Use Library's Subscription Management**
|
|
1818
|
+
- Replace custom subscription tracking with library's KV store
|
|
1819
|
+
- Leverage automatic webhook routing
|
|
1820
|
+
|
|
1821
|
+
3. **Standardize Error Handling**
|
|
1822
|
+
- Use MCP error codes
|
|
1823
|
+
- Implement standard error responses
|
|
1824
|
+
|
|
1825
|
+
**Reference:**
|
|
1826
|
+
- [Migration Examples](#)
|
|
1827
|
+
- [Comparison Guide](#)
|
|
1828
|
+
|
|
1829
|
+
---
|
|
1830
|
+
|
|
1831
|
+
## 19. Troubleshooting
|
|
1832
|
+
|
|
1833
|
+
### 19.1 Common Issues
|
|
1834
|
+
|
|
1835
|
+
**Webhook Not Received:**
|
|
1836
|
+
|
|
1837
|
+
```bash
|
|
1838
|
+
# Check webhook registration
|
|
1839
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
1840
|
+
https://api.github.com/repos/owner/repo/hooks
|
|
1841
|
+
|
|
1842
|
+
# Test webhook delivery
|
|
1843
|
+
curl -X POST https://mcp.example.com/webhooks/incoming/sub_xyz \
|
|
1844
|
+
-H "Content-Type: application/json" \
|
|
1845
|
+
-d '{"test": true}'
|
|
1846
|
+
|
|
1847
|
+
# Check server logs
|
|
1848
|
+
docker logs mcp-server | grep "webhook"
|
|
1849
|
+
|
|
1850
|
+
# Verify subscription exists in store
|
|
1851
|
+
redis-cli GET "subscription:sub_xyz"
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
**Client Not Receiving Notifications:**
|
|
1855
|
+
|
|
1856
|
+
```typescript
|
|
1857
|
+
// Enable debug logging
|
|
1858
|
+
const server = createMCPServer({
|
|
1859
|
+
// ...
|
|
1860
|
+
logLevel: 'debug',
|
|
1861
|
+
webhooks: {
|
|
1862
|
+
outgoing: {
|
|
1863
|
+
timeout: 5000,
|
|
1864
|
+
retries: 3,
|
|
1865
|
+
|
|
1866
|
+
// Add debugging
|
|
1867
|
+
onBeforeCall: (url, payload) => {
|
|
1868
|
+
console.log('Calling client webhook:', url, payload);
|
|
1869
|
+
},
|
|
1870
|
+
onAfterCall: (url, response, error) => {
|
|
1871
|
+
console.log('Webhook result:', { url, status: response?.status, error });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
**Store Connection Issues:**
|
|
1879
|
+
|
|
1880
|
+
```typescript
|
|
1881
|
+
// Test store connectivity
|
|
1882
|
+
async function testStore(store: KeyValueStore) {
|
|
1883
|
+
try {
|
|
1884
|
+
await store.set('test-key', 'test-value', 60);
|
|
1885
|
+
const value = await store.get('test-key');
|
|
1886
|
+
await store.delete('test-key');
|
|
1887
|
+
|
|
1888
|
+
if (value === 'test-value') {
|
|
1889
|
+
console.log('✓ Store connection OK');
|
|
1890
|
+
} else {
|
|
1891
|
+
console.error('✗ Store read/write mismatch');
|
|
1892
|
+
}
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
console.error('✗ Store connection failed:', error);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
**Signature Verification Failures:**
|
|
1900
|
+
|
|
1901
|
+
```typescript
|
|
1902
|
+
// Debug signature verification
|
|
1903
|
+
verifyIncomingSignature: (payload, signature, secret) => {
|
|
1904
|
+
console.log('Verifying signature:');
|
|
1905
|
+
console.log('- Received signature:', signature);
|
|
1906
|
+
console.log('- Payload:', JSON.stringify(payload));
|
|
1907
|
+
console.log('- Secret length:', secret?.length);
|
|
1908
|
+
|
|
1909
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
1910
|
+
hmac.update(JSON.stringify(payload));
|
|
1911
|
+
const expected = `sha256=${hmac.digest('hex')}`;
|
|
1912
|
+
|
|
1913
|
+
console.log('- Expected signature:', expected);
|
|
1914
|
+
console.log('- Match:', signature === expected);
|
|
1915
|
+
|
|
1916
|
+
return signature === expected;
|
|
1917
|
+
}
|
|
1918
|
+
```
|
|
1919
|
+
|
|
1920
|
+
### 19.2 Debugging Tools
|
|
1921
|
+
|
|
1922
|
+
**List All Subscriptions:**
|
|
1923
|
+
|
|
1924
|
+
```typescript
|
|
1925
|
+
// GET /debug/subscriptions (development only)
|
|
1926
|
+
async function listAllSubscriptions(store: KeyValueStore) {
|
|
1927
|
+
const keys = await store.scan('subscription:*');
|
|
1928
|
+
const subscriptions = await Promise.all(
|
|
1929
|
+
keys.map(async (key) => {
|
|
1930
|
+
const data = await store.get(key);
|
|
1931
|
+
return { key, data: JSON.parse(data || '{}') };
|
|
1932
|
+
})
|
|
1933
|
+
);
|
|
1934
|
+
return subscriptions;
|
|
1935
|
+
}
|
|
1936
|
+
```
|
|
1937
|
+
|
|
1938
|
+
**Test Webhook Delivery:**
|
|
1939
|
+
|
|
1940
|
+
```bash
|
|
1941
|
+
# Simulate third-party webhook
|
|
1942
|
+
curl -X POST http://localhost:3000/webhooks/incoming/sub_xyz \
|
|
1943
|
+
-H "Content-Type: application/json" \
|
|
1944
|
+
-H "X-GitHub-Event: issues" \
|
|
1945
|
+
-H "X-Hub-Signature-256: sha256=..." \
|
|
1946
|
+
-d @webhook-payload.json
|
|
1947
|
+
```
|
|
1948
|
+
|
|
1949
|
+
**Monitor Webhook Queue:**
|
|
1950
|
+
|
|
1951
|
+
```typescript
|
|
1952
|
+
// Check dead letter queue
|
|
1953
|
+
async function checkDLQ(store: KeyValueStore) {
|
|
1954
|
+
const failedWebhooks = await store.scan('dlq:*');
|
|
1955
|
+
console.log(`Failed webhooks: ${failedWebhooks.length}`);
|
|
1956
|
+
|
|
1957
|
+
for (const key of failedWebhooks) {
|
|
1958
|
+
const data = await store.get(key);
|
|
1959
|
+
console.log(JSON.parse(data || '{}'));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
```
|
|
1963
|
+
|
|
1964
|
+
**Reference:**
|
|
1965
|
+
- [Troubleshooting Guide](#)
|
|
1966
|
+
- [Debug Mode Documentation](#)
|
|
1967
|
+
- [FAQ](#)
|
|
1968
|
+
|
|
1969
|
+
---
|
|
1970
|
+
|
|
1971
|
+
## 20. API Reference
|
|
1972
|
+
|
|
1973
|
+
### 20.1 Core Types
|
|
1974
|
+
|
|
1975
|
+
```typescript
|
|
1976
|
+
// Server configuration
|
|
1977
|
+
interface MCPServerConfig { /* ... */ }
|
|
1978
|
+
|
|
1979
|
+
// Tool definition
|
|
1980
|
+
interface ToolDefinition<TInput, TOutput> { /* ... */ }
|
|
1981
|
+
|
|
1982
|
+
// Resource definition
|
|
1983
|
+
interface ResourceDefinition<TData> { /* ... */ }
|
|
1984
|
+
|
|
1985
|
+
// Subscription handlers
|
|
1986
|
+
interface ResourceSubscription { /* ... */ }
|
|
1987
|
+
|
|
1988
|
+
// Storage interface
|
|
1989
|
+
interface KeyValueStore { /* ... */ }
|
|
1990
|
+
|
|
1991
|
+
// Webhook configuration
|
|
1992
|
+
interface WebhookConfig { /* ... */ }
|
|
1993
|
+
|
|
1994
|
+
// Authentication context
|
|
1995
|
+
interface AuthContext { /* ... */ }
|
|
1996
|
+
```
|
|
1997
|
+
|
|
1998
|
+
Full type definitions: [API Types Documentation](#)
|
|
1999
|
+
|
|
2000
|
+
### 20.2 Utility Functions
|
|
2001
|
+
|
|
2002
|
+
**URI Template Parsing:**
|
|
2003
|
+
|
|
2004
|
+
```typescript
|
|
2005
|
+
import { parseUriTemplate, matchUriTemplate } from 'mcp-http-webhook/utils';
|
|
2006
|
+
|
|
2007
|
+
const params = parseUriTemplate(
|
|
2008
|
+
'github://repo/{owner}/{repo}/issues',
|
|
2009
|
+
'github://repo/octocat/hello-world/issues'
|
|
2010
|
+
);
|
|
2011
|
+
// { owner: 'octocat', repo: 'hello-world' }
|
|
2012
|
+
|
|
2013
|
+
const matches = matchUriTemplate(
|
|
2014
|
+
'github://repo/{owner}/{repo}/issues',
|
|
2015
|
+
'github://repo/octocat/hello-world/issues'
|
|
2016
|
+
);
|
|
2017
|
+
// true
|
|
2018
|
+
```
|
|
2019
|
+
|
|
2020
|
+
**ID Generation:**
|
|
2021
|
+
|
|
2022
|
+
```typescript
|
|
2023
|
+
import { generateSubscriptionId, generateWebhookUrl } from 'mcp-http-webhook/utils';
|
|
2024
|
+
|
|
2025
|
+
const id = generateSubscriptionId();
|
|
2026
|
+
// 'sub_1a2b3c4d5e6f'
|
|
2027
|
+
|
|
2028
|
+
const url = generateWebhookUrl(publicUrl, subscriptionId);
|
|
2029
|
+
// 'https://mcp.example.com/webhooks/incoming/sub_1a2b3c4d5e6f'
|
|
2030
|
+
```
|
|
2031
|
+
|
|
2032
|
+
**Signature Utilities:**
|
|
2033
|
+
|
|
2034
|
+
```typescript
|
|
2035
|
+
import { createHmacSignature, verifyHmacSignature } from 'mcp-http-webhook/utils';
|
|
2036
|
+
|
|
2037
|
+
const signature = createHmacSignature(payload, secret, 'sha256');
|
|
2038
|
+
const isValid = verifyHmacSignature(payload, signature, secret, 'sha256');
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
**Reference:**
|
|
2042
|
+
- [Utility Functions Documentation](#)
|
|
2043
|
+
- [Helper Methods](#)
|
|
2044
|
+
|
|
2045
|
+
---
|
|
2046
|
+
|
|
2047
|
+
## 21. Examples Repository
|
|
2048
|
+
|
|
2049
|
+
### 21.1 Complete Examples
|
|
2050
|
+
|
|
2051
|
+
- **GitHub Integration** - [github-mcp-server.ts](#)
|
|
2052
|
+
- Issue tracking, PR management, webhook subscriptions
|
|
2053
|
+
|
|
2054
|
+
- **Google Drive Integration** - [gdrive-mcp-server.ts](#)
|
|
2055
|
+
- File watching, change notifications via Drive API webhooks
|
|
2056
|
+
|
|
2057
|
+
- **Slack Integration** - [slack-mcp-server.ts](#)
|
|
2058
|
+
- Channel messages, event subscriptions, slash commands
|
|
2059
|
+
|
|
2060
|
+
- **PostgreSQL Changes** - [postgres-mcp-server.ts](#)
|
|
2061
|
+
- LISTEN/NOTIFY based resource subscriptions
|
|
2062
|
+
|
|
2063
|
+
- **Stripe Webhooks** - [stripe-mcp-server.ts](#)
|
|
2064
|
+
- Payment events, subscription changes
|
|
2065
|
+
|
|
2066
|
+
- **Shopify Integration** - [shopify-mcp-server.ts](#)
|
|
2067
|
+
- Product updates, order notifications
|
|
2068
|
+
|
|
2069
|
+
### 21.2 Store Implementations
|
|
2070
|
+
|
|
2071
|
+
- **Redis** - [redis-store.ts](#)
|
|
2072
|
+
- **DynamoDB** - [dynamodb-store.ts](#)
|
|
2073
|
+
- **PostgreSQL** - [postgres-store.ts](#)
|
|
2074
|
+
- **MongoDB** - [mongodb-store.ts](#)
|
|
2075
|
+
- **In-Memory (Dev)** - [memory-store.ts](#)
|
|
2076
|
+
|
|
2077
|
+
### 21.3 Authentication Examples
|
|
2078
|
+
|
|
2079
|
+
- **JWT Bearer Token** - [jwt-auth.ts](#)
|
|
2080
|
+
- **API Key** - [apikey-auth.ts](#)
|
|
2081
|
+
- **OAuth2** - [oauth2-auth.ts](#)
|
|
2082
|
+
- **Mutual TLS** - [mtls-auth.ts](#)
|
|
2083
|
+
|
|
2084
|
+
**Reference:**
|
|
2085
|
+
- [Examples Repository](https://github.com/your-org/mcp-http-webhook/tree/main/examples)
|
|
2086
|
+
|
|
2087
|
+
---
|
|
2088
|
+
|
|
2089
|
+
## 22. Performance Optimization
|
|
2090
|
+
|
|
2091
|
+
### 22.1 Connection Pooling
|
|
2092
|
+
|
|
2093
|
+
**Redis Connection Pool:**
|
|
2094
|
+
|
|
2095
|
+
```typescript
|
|
2096
|
+
import Redis from 'ioredis';
|
|
2097
|
+
|
|
2098
|
+
const redis = new Redis({
|
|
2099
|
+
host: process.env.REDIS_HOST,
|
|
2100
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
2101
|
+
maxRetriesPerRequest: 3,
|
|
2102
|
+
enableReadyCheck: true,
|
|
2103
|
+
lazyConnect: false,
|
|
2104
|
+
|
|
2105
|
+
// Connection pool settings
|
|
2106
|
+
connectionName: 'mcp-server',
|
|
2107
|
+
connectTimeout: 10000,
|
|
2108
|
+
|
|
2109
|
+
// Reconnection strategy
|
|
2110
|
+
retryStrategy: (times) => {
|
|
2111
|
+
const delay = Math.min(times * 50, 2000);
|
|
2112
|
+
return delay;
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
**HTTP Client Pool:**
|
|
2118
|
+
|
|
2119
|
+
```typescript
|
|
2120
|
+
import { Agent } from 'https';
|
|
2121
|
+
|
|
2122
|
+
const httpsAgent = new Agent({
|
|
2123
|
+
keepAlive: true,
|
|
2124
|
+
maxSockets: 50,
|
|
2125
|
+
maxFreeSockets: 10,
|
|
2126
|
+
timeout: 60000,
|
|
2127
|
+
keepAliveMsecs: 30000
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
// Use with fetch or axios
|
|
2131
|
+
fetch(url, { agent: httpsAgent });
|
|
2132
|
+
```
|
|
2133
|
+
|
|
2134
|
+
### 22.2 Caching Strategies
|
|
2135
|
+
|
|
2136
|
+
**Multi-Level Caching:**
|
|
2137
|
+
|
|
2138
|
+
```typescript
|
|
2139
|
+
import NodeCache from 'node-cache';
|
|
2140
|
+
|
|
2141
|
+
const memoryCache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
|
|
2142
|
+
|
|
2143
|
+
const cachedRead = async (uri: string, context: AuthContext) => {
|
|
2144
|
+
// L1: Memory cache (fastest)
|
|
2145
|
+
const memoryCached = memoryCache.get(uri);
|
|
2146
|
+
if (memoryCached) return memoryCached;
|
|
2147
|
+
|
|
2148
|
+
// L2: Redis cache
|
|
2149
|
+
const redisCached = await redis.get(`cache:${uri}`);
|
|
2150
|
+
if (redisCached) {
|
|
2151
|
+
const data = JSON.parse(redisCached);
|
|
2152
|
+
memoryCache.set(uri, data);
|
|
2153
|
+
return data;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// L3: Fetch from source
|
|
2157
|
+
const data = await fetchFromSource(uri, context);
|
|
2158
|
+
|
|
2159
|
+
// Update caches
|
|
2160
|
+
memoryCache.set(uri, data);
|
|
2161
|
+
await redis.setex(`cache:${uri}`, 300, JSON.stringify(data));
|
|
2162
|
+
|
|
2163
|
+
return data;
|
|
2164
|
+
};
|
|
2165
|
+
```
|
|
2166
|
+
|
|
2167
|
+
### 22.3 Batch Processing
|
|
2168
|
+
|
|
2169
|
+
**Webhook Batching:**
|
|
2170
|
+
|
|
2171
|
+
```typescript
|
|
2172
|
+
class WebhookBatcher {
|
|
2173
|
+
private queue: Map<string, any[]> = new Map();
|
|
2174
|
+
private timer: NodeJS.Timeout | null = null;
|
|
2175
|
+
|
|
2176
|
+
add(clientUrl: string, notification: any) {
|
|
2177
|
+
if (!this.queue.has(clientUrl)) {
|
|
2178
|
+
this.queue.set(clientUrl, []);
|
|
2179
|
+
}
|
|
2180
|
+
this.queue.get(clientUrl)!.push(notification);
|
|
2181
|
+
|
|
2182
|
+
if (!this.timer) {
|
|
2183
|
+
this.timer = setTimeout(() => this.flush(), 1000);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
async flush() {
|
|
2188
|
+
const batches = Array.from(this.queue.entries());
|
|
2189
|
+
this.queue.clear();
|
|
2190
|
+
this.timer = null;
|
|
2191
|
+
|
|
2192
|
+
await Promise.all(
|
|
2193
|
+
batches.map(([url, notifications]) =>
|
|
2194
|
+
this.sendBatch(url, notifications)
|
|
2195
|
+
)
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
async sendBatch(url: string, notifications: any[]) {
|
|
2200
|
+
// Send all notifications in one request
|
|
2201
|
+
await fetch(url, {
|
|
2202
|
+
method: 'POST',
|
|
2203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2204
|
+
body: JSON.stringify({ batch: notifications })
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
```
|
|
2209
|
+
|
|
2210
|
+
**Reference:**
|
|
2211
|
+
- [Performance Tuning Guide](#)
|
|
2212
|
+
- [Benchmarking Tools](#)
|
|
2213
|
+
- [Optimization Examples](#)
|
|
2214
|
+
|
|
2215
|
+
---
|
|
2216
|
+
|
|
2217
|
+
## 23. Contributing
|
|
2218
|
+
|
|
2219
|
+
### 23.1 Development Setup
|
|
2220
|
+
|
|
2221
|
+
```bash
|
|
2222
|
+
# Clone repository
|
|
2223
|
+
git clone https://github.com/your-org/mcp-http-webhook.git
|
|
2224
|
+
cd mcp-http-webhook
|
|
2225
|
+
|
|
2226
|
+
# Install dependencies
|
|
2227
|
+
pnpm install
|
|
2228
|
+
|
|
2229
|
+
# Run tests
|
|
2230
|
+
pnpm test
|
|
2231
|
+
|
|
2232
|
+
# Start development server
|
|
2233
|
+
pnpm dev
|
|
2234
|
+
|
|
2235
|
+
# Build
|
|
2236
|
+
pnpm build
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
### 23.2 Project Structure
|
|
2240
|
+
|
|
2241
|
+
```
|
|
2242
|
+
mcp-http-webhook/
|
|
2243
|
+
├── src/
|
|
2244
|
+
│ ├── index.ts # Main export
|
|
2245
|
+
│ ├── server.ts # Server creation
|
|
2246
|
+
│ ├── protocol/ # MCP protocol implementation
|
|
2247
|
+
│ ├── webhooks/ # Webhook handling
|
|
2248
|
+
│ ├── subscriptions/ # Subscription management
|
|
2249
|
+
│ ├── utils/ # Utility functions
|
|
2250
|
+
│ └── types/ # TypeScript types
|
|
2251
|
+
├── examples/ # Example implementations
|
|
2252
|
+
├── tests/ # Test suites
|
|
2253
|
+
├── docs/ # Documentation
|
|
2254
|
+
└── scripts/ # Build and utility scripts
|
|
2255
|
+
```
|
|
2256
|
+
|
|
2257
|
+
### 23.3 Coding Standards
|
|
2258
|
+
|
|
2259
|
+
**TypeScript Guidelines:**
|
|
2260
|
+
|
|
2261
|
+
```typescript
|
|
2262
|
+
// Use explicit types for public APIs
|
|
2263
|
+
export interface ToolDefinition<TInput = any, TOutput = any> {
|
|
2264
|
+
name: string;
|
|
2265
|
+
description: string;
|
|
2266
|
+
inputSchema: JSONSchema;
|
|
2267
|
+
handler: ToolHandler<TInput, TOutput>;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Use async/await over promises
|
|
2271
|
+
async function fetchData(): Promise<Data> {
|
|
2272
|
+
return await api.get('/data');
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// Prefer const over let
|
|
2276
|
+
const config = getConfig();
|
|
2277
|
+
|
|
2278
|
+
// Use optional chaining and nullish coalescing
|
|
2279
|
+
const value = config?.webhooks?.timeout ?? 5000;
|
|
2280
|
+
|
|
2281
|
+
// Document public APIs with JSDoc
|
|
2282
|
+
/**
|
|
2283
|
+
* Creates a new MCP server instance
|
|
2284
|
+
* @param config - Server configuration
|
|
2285
|
+
* @returns Configured MCP server
|
|
2286
|
+
* @throws {ValidationError} If configuration is invalid
|
|
2287
|
+
*/
|
|
2288
|
+
export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
2289
|
+
// ...
|
|
2290
|
+
}
|
|
2291
|
+
```
|
|
2292
|
+
|
|
2293
|
+
**Code Style:**
|
|
2294
|
+
|
|
2295
|
+
```typescript
|
|
2296
|
+
// Naming conventions
|
|
2297
|
+
class SubscriptionManager { } // PascalCase for classes
|
|
2298
|
+
function handleWebhook() { } // camelCase for functions
|
|
2299
|
+
const MAX_RETRIES = 3; // UPPER_CASE for constants
|
|
2300
|
+
interface WebhookConfig { } // PascalCase for interfaces
|
|
2301
|
+
|
|
2302
|
+
// Import ordering
|
|
2303
|
+
import { external } from 'external'; // External packages
|
|
2304
|
+
import { internal } from './internal'; // Internal modules
|
|
2305
|
+
import type { Type } from './types'; // Type imports last
|
|
2306
|
+
|
|
2307
|
+
// Error handling
|
|
2308
|
+
try {
|
|
2309
|
+
await riskyOperation();
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
logger.error('Operation failed', { error, context });
|
|
2312
|
+
throw new OperationError('Failed to complete', { cause: error });
|
|
2313
|
+
}
|
|
2314
|
+
```
|
|
2315
|
+
|
|
2316
|
+
### 23.4 Testing Requirements
|
|
2317
|
+
|
|
2318
|
+
**Test Coverage Goals:**
|
|
2319
|
+
- Unit tests: >80% coverage
|
|
2320
|
+
- Integration tests for all major flows
|
|
2321
|
+
- E2E tests for critical paths
|
|
2322
|
+
|
|
2323
|
+
**Test Structure:**
|
|
2324
|
+
|
|
2325
|
+
```typescript
|
|
2326
|
+
describe('SubscriptionManager', () => {
|
|
2327
|
+
let manager: SubscriptionManager;
|
|
2328
|
+
let mockStore: jest.Mocked<KeyValueStore>;
|
|
2329
|
+
|
|
2330
|
+
beforeEach(() => {
|
|
2331
|
+
mockStore = {
|
|
2332
|
+
get: jest.fn(),
|
|
2333
|
+
set: jest.fn(),
|
|
2334
|
+
delete: jest.fn()
|
|
2335
|
+
};
|
|
2336
|
+
manager = new SubscriptionManager(mockStore);
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
describe('createSubscription', () => {
|
|
2340
|
+
it('should create a new subscription', async () => {
|
|
2341
|
+
const result = await manager.createSubscription({
|
|
2342
|
+
uri: 'test://resource',
|
|
2343
|
+
clientCallbackUrl: 'http://client.test/webhook'
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
expect(result.subscriptionId).toBeDefined();
|
|
2347
|
+
expect(mockStore.set).toHaveBeenCalled();
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
it('should throw error for invalid URI', async () => {
|
|
2351
|
+
await expect(
|
|
2352
|
+
manager.createSubscription({
|
|
2353
|
+
uri: 'invalid',
|
|
2354
|
+
clientCallbackUrl: 'http://client.test/webhook'
|
|
2355
|
+
})
|
|
2356
|
+
).rejects.toThrow(ValidationError);
|
|
2357
|
+
});
|
|
2358
|
+
});
|
|
2359
|
+
});
|
|
2360
|
+
```
|
|
2361
|
+
|
|
2362
|
+
### 23.5 Pull Request Process
|
|
2363
|
+
|
|
2364
|
+
1. **Fork and Branch**
|
|
2365
|
+
```bash
|
|
2366
|
+
git checkout -b feature/my-new-feature
|
|
2367
|
+
```
|
|
2368
|
+
|
|
2369
|
+
2. **Make Changes**
|
|
2370
|
+
- Write code following style guide
|
|
2371
|
+
- Add/update tests
|
|
2372
|
+
- Update documentation
|
|
2373
|
+
|
|
2374
|
+
3. **Run Quality Checks**
|
|
2375
|
+
```bash
|
|
2376
|
+
pnpm lint # ESLint
|
|
2377
|
+
pnpm format # Prettier
|
|
2378
|
+
pnpm test # Jest tests
|
|
2379
|
+
pnpm type-check # TypeScript
|
|
2380
|
+
```
|
|
2381
|
+
|
|
2382
|
+
4. **Commit with Conventional Commits**
|
|
2383
|
+
```bash
|
|
2384
|
+
git commit -m "feat: add webhook retry backoff configuration"
|
|
2385
|
+
git commit -m "fix: resolve subscription cleanup race condition"
|
|
2386
|
+
git commit -m "docs: update subscription flow diagram"
|
|
2387
|
+
```
|
|
2388
|
+
|
|
2389
|
+
5. **Submit PR**
|
|
2390
|
+
- Clear description of changes
|
|
2391
|
+
- Link to related issues
|
|
2392
|
+
- Include screenshots/examples if applicable
|
|
2393
|
+
|
|
2394
|
+
**Reference:**
|
|
2395
|
+
- [Contributing Guide](https://github.com/your-org/mcp-http-webhook/blob/main/CONTRIBUTING.md)
|
|
2396
|
+
- [Code of Conduct](https://github.com/your-org/mcp-http-webhook/blob/main/CODE_OF_CONDUCT.md)
|
|
2397
|
+
|
|
2398
|
+
---
|
|
2399
|
+
|
|
2400
|
+
## 24. Changelog
|
|
2401
|
+
|
|
2402
|
+
### Version 1.0.0 (2025-01-15)
|
|
2403
|
+
|
|
2404
|
+
**Initial Release**
|
|
2405
|
+
|
|
2406
|
+
**Features:**
|
|
2407
|
+
- ✨ HTTP-based MCP server implementation
|
|
2408
|
+
- ✨ Webhook-based resource subscriptions
|
|
2409
|
+
- ✨ Key-value store abstraction for persistence
|
|
2410
|
+
- ✨ Automatic webhook routing and retry logic
|
|
2411
|
+
- ✨ Signature verification for incoming/outgoing webhooks
|
|
2412
|
+
- ✨ Support for tools, resources, and prompts
|
|
2413
|
+
- ✨ Built-in authentication middleware
|
|
2414
|
+
- ✨ Prometheus metrics integration
|
|
2415
|
+
- ✨ OpenTelemetry tracing support
|
|
2416
|
+
- ✨ Dead letter queue for failed webhooks
|
|
2417
|
+
|
|
2418
|
+
**Storage Implementations:**
|
|
2419
|
+
- 📦 Redis adapter
|
|
2420
|
+
- 📦 DynamoDB adapter
|
|
2421
|
+
- 📦 PostgreSQL adapter
|
|
2422
|
+
- 📦 In-memory adapter (development)
|
|
2423
|
+
|
|
2424
|
+
**Examples:**
|
|
2425
|
+
- 📚 GitHub integration
|
|
2426
|
+
- 📚 Google Drive integration
|
|
2427
|
+
- 📚 Slack integration
|
|
2428
|
+
- 📚 PostgreSQL NOTIFY integration
|
|
2429
|
+
- 📚 Stripe webhooks
|
|
2430
|
+
|
|
2431
|
+
**Documentation:**
|
|
2432
|
+
- 📖 Complete API reference
|
|
2433
|
+
- 📖 Deployment guides (Docker, Kubernetes)
|
|
2434
|
+
- 📖 Security best practices
|
|
2435
|
+
- 📖 Performance optimization guide
|
|
2436
|
+
- 📖 Migration guide from standard MCP
|
|
2437
|
+
|
|
2438
|
+
---
|
|
2439
|
+
|
|
2440
|
+
## 25. Roadmap
|
|
2441
|
+
|
|
2442
|
+
### Version 1.1.0 (Q2 2025)
|
|
2443
|
+
|
|
2444
|
+
**Planned Features:**
|
|
2445
|
+
- 🚀 GraphQL subscription support
|
|
2446
|
+
- 🚀 WebSocket fallback transport
|
|
2447
|
+
- 🚀 Built-in rate limiting per user
|
|
2448
|
+
- 🚀 Subscription priority levels
|
|
2449
|
+
- 🚀 Webhook payload transformation
|
|
2450
|
+
- 🚀 Multi-region deployment support
|
|
2451
|
+
- 🚀 Admin dashboard UI
|
|
2452
|
+
|
|
2453
|
+
### Version 1.2.0 (Q3 2025)
|
|
2454
|
+
|
|
2455
|
+
**Planned Features:**
|
|
2456
|
+
- 🚀 gRPC transport option
|
|
2457
|
+
- 🚀 Event sourcing support
|
|
2458
|
+
- 🚀 Built-in circuit breaker
|
|
2459
|
+
- 🚀 A/B testing framework
|
|
2460
|
+
- 🚀 Subscription groups/topics
|
|
2461
|
+
- 🚀 Webhook replay functionality
|
|
2462
|
+
|
|
2463
|
+
### Future Considerations
|
|
2464
|
+
- Cloud provider integrations (AWS EventBridge, Azure Event Grid)
|
|
2465
|
+
- Machine learning-based anomaly detection
|
|
2466
|
+
- Advanced analytics and insights
|
|
2467
|
+
- Multi-tenancy support
|
|
2468
|
+
- Federation across multiple MCP servers
|
|
2469
|
+
|
|
2470
|
+
**Feature Requests:**
|
|
2471
|
+
Submit feature requests at [GitHub Issues](https://github.com/your-org/mcp-http-webhook/issues)
|
|
2472
|
+
|
|
2473
|
+
---
|
|
2474
|
+
|
|
2475
|
+
## 26. FAQ
|
|
2476
|
+
|
|
2477
|
+
### General Questions
|
|
2478
|
+
|
|
2479
|
+
**Q: How is this different from standard MCP with SSE transport?**
|
|
2480
|
+
|
|
2481
|
+
A: This library eliminates persistent connections entirely. Instead of maintaining an SSE connection for push notifications, clients provide a webhook URL and receive notifications via HTTP POST. This makes horizontal scaling trivial and works better with serverless architectures.
|
|
2482
|
+
|
|
2483
|
+
**Q: Can I use this with the standard MCP clients?**
|
|
2484
|
+
|
|
2485
|
+
A: No, standard MCP clients expect SSE transport. You'll need to implement a client that works with HTTP + webhooks. However, the protocol messages (tools/call, resources/read, etc.) remain the same.
|
|
2486
|
+
|
|
2487
|
+
**Q: Why do I need an external key-value store?**
|
|
2488
|
+
|
|
2489
|
+
A: Subscription state must be shared across multiple server instances. An external store (Redis, DynamoDB, etc.) enables horizontal scaling and prevents data loss during deployments.
|
|
2490
|
+
|
|
2491
|
+
### Subscription Questions
|
|
2492
|
+
|
|
2493
|
+
**Q: What happens if a client's webhook endpoint is down?**
|
|
2494
|
+
|
|
2495
|
+
A: The library automatically retries with exponential backoff (configurable). After all retries fail, the notification is stored in the dead letter queue for manual intervention.
|
|
2496
|
+
|
|
2497
|
+
**Q: Can one resource have multiple subscriptions from the same client?**
|
|
2498
|
+
|
|
2499
|
+
A: Yes, each subscription gets a unique `subscriptionId`. A client can subscribe to the same resource multiple times with different callback URLs.
|
|
2500
|
+
|
|
2501
|
+
**Q: How do I test subscriptions locally without exposing a public URL?**
|
|
2502
|
+
|
|
2503
|
+
A: Use tools like [ngrok](https://ngrok.com/) or [localhost.run](https://localhost.run/) to create temporary public URLs that tunnel to your local machine.
|
|
2504
|
+
|
|
2505
|
+
```bash
|
|
2506
|
+
ngrok http 3000
|
|
2507
|
+
# Use the generated URL as your publicUrl
|
|
2508
|
+
```
|
|
2509
|
+
|
|
2510
|
+
### Third-Party Integration Questions
|
|
2511
|
+
|
|
2512
|
+
**Q: What if the third-party service doesn't support webhooks?**
|
|
2513
|
+
|
|
2514
|
+
A: You can implement polling-based subscriptions:
|
|
2515
|
+
|
|
2516
|
+
```typescript
|
|
2517
|
+
subscription: {
|
|
2518
|
+
onSubscribe: async (uri, subscriptionId, webhookUrl, context) => {
|
|
2519
|
+
// Start a polling job
|
|
2520
|
+
const jobId = await queue.add('poll-resource', {
|
|
2521
|
+
uri,
|
|
2522
|
+
subscriptionId,
|
|
2523
|
+
interval: 60000 // Poll every minute
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
return { thirdPartyWebhookId: jobId };
|
|
2527
|
+
},
|
|
2528
|
+
|
|
2529
|
+
// In your polling worker:
|
|
2530
|
+
// - Fetch resource data
|
|
2531
|
+
// - Compare with previous state
|
|
2532
|
+
// - If changed, POST to webhookUrl
|
|
2533
|
+
}
|
|
2534
|
+
```
|
|
2535
|
+
|
|
2536
|
+
**Q: How do I handle webhook signature verification for different third-party services?**
|
|
2537
|
+
|
|
2538
|
+
A: Each resource can implement custom verification logic:
|
|
2539
|
+
|
|
2540
|
+
```typescript
|
|
2541
|
+
onWebhook: async (subscriptionId, payload, headers) => {
|
|
2542
|
+
// GitHub uses X-Hub-Signature-256
|
|
2543
|
+
if (headers['x-hub-signature-256']) {
|
|
2544
|
+
verifyGitHubSignature(payload, headers['x-hub-signature-256'], secret);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Slack uses X-Slack-Signature
|
|
2548
|
+
if (headers['x-slack-signature']) {
|
|
2549
|
+
verifySlackSignature(payload, headers['x-slack-signature'], headers['x-slack-request-timestamp'], secret);
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// Process webhook...
|
|
2553
|
+
}
|
|
2554
|
+
```
|
|
2555
|
+
|
|
2556
|
+
### Performance Questions
|
|
2557
|
+
|
|
2558
|
+
**Q: How many subscriptions can the server handle?**
|
|
2559
|
+
|
|
2560
|
+
A: This depends on your key-value store. Redis can easily handle millions of subscriptions. The bottleneck is typically outgoing webhook calls, which can be optimized with batching and connection pooling.
|
|
2561
|
+
|
|
2562
|
+
**Q: What's the latency for webhook notifications?**
|
|
2563
|
+
|
|
2564
|
+
A: Typically <500ms from third-party webhook receipt to client notification, depending on network conditions and your webhook processing logic.
|
|
2565
|
+
|
|
2566
|
+
**Q: Should I batch webhook notifications?**
|
|
2567
|
+
|
|
2568
|
+
A: Yes, if you expect high-frequency updates. The library supports batching multiple notifications into a single HTTP call to clients.
|
|
2569
|
+
|
|
2570
|
+
### Security Questions
|
|
2571
|
+
|
|
2572
|
+
**Q: How do I prevent replay attacks on webhooks?**
|
|
2573
|
+
|
|
2574
|
+
A: Implement timestamp verification:
|
|
2575
|
+
|
|
2576
|
+
```typescript
|
|
2577
|
+
verifyIncomingSignature: (payload, signature, secret) => {
|
|
2578
|
+
const timestamp = headers['x-webhook-timestamp'];
|
|
2579
|
+
const now = Date.now() / 1000;
|
|
2580
|
+
|
|
2581
|
+
// Reject webhooks older than 5 minutes
|
|
2582
|
+
if (Math.abs(now - parseInt(timestamp)) > 300) {
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Verify signature including timestamp
|
|
2587
|
+
return verifySignature(payload, signature, secret);
|
|
2588
|
+
}
|
|
2589
|
+
```
|
|
2590
|
+
|
|
2591
|
+
**Q: Should I whitelist client callback URLs?**
|
|
2592
|
+
|
|
2593
|
+
A: Yes, for security:
|
|
2594
|
+
|
|
2595
|
+
```typescript
|
|
2596
|
+
const ALLOWED_DOMAINS = ['client.example.com', 'app.client.com'];
|
|
2597
|
+
|
|
2598
|
+
authenticate: async (req) => {
|
|
2599
|
+
const callbackUrl = req.body.params?.callbackUrl;
|
|
2600
|
+
if (callbackUrl) {
|
|
2601
|
+
const domain = new URL(callbackUrl).hostname;
|
|
2602
|
+
if (!ALLOWED_DOMAINS.includes(domain)) {
|
|
2603
|
+
throw new ValidationError('Callback URL not whitelisted');
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
// ... continue authentication
|
|
2607
|
+
}
|
|
2608
|
+
```
|
|
2609
|
+
|
|
2610
|
+
### Deployment Questions
|
|
2611
|
+
|
|
2612
|
+
**Q: Can I run this serverlessly (AWS Lambda, Cloud Functions)?**
|
|
2613
|
+
|
|
2614
|
+
A: Yes, but with caveats:
|
|
2615
|
+
- Tools and resources work perfectly (stateless HTTP)
|
|
2616
|
+
- Incoming webhooks work fine
|
|
2617
|
+
- Outgoing webhook calls need to handle cold starts
|
|
2618
|
+
- Consider using a separate always-on service for webhook processing
|
|
2619
|
+
|
|
2620
|
+
**Q: How do I handle zero-downtime deployments?**
|
|
2621
|
+
|
|
2622
|
+
A:
|
|
2623
|
+
1. Use external store (Redis/DynamoDB) for state
|
|
2624
|
+
2. Deploy new version alongside old
|
|
2625
|
+
3. Update load balancer to point to new version
|
|
2626
|
+
4. Old version continues processing in-flight webhooks
|
|
2627
|
+
5. Gracefully shutdown old version after drain period
|
|
2628
|
+
|
|
2629
|
+
```typescript
|
|
2630
|
+
process.on('SIGTERM', async () => {
|
|
2631
|
+
console.log('Received SIGTERM, starting graceful shutdown');
|
|
2632
|
+
|
|
2633
|
+
// Stop accepting new connections
|
|
2634
|
+
server.close();
|
|
2635
|
+
|
|
2636
|
+
// Wait for in-flight requests to complete
|
|
2637
|
+
await waitForInFlightRequests();
|
|
2638
|
+
|
|
2639
|
+
// Close store connections
|
|
2640
|
+
await redis.quit();
|
|
2641
|
+
|
|
2642
|
+
process.exit(0);
|
|
2643
|
+
});
|
|
2644
|
+
```
|
|
2645
|
+
|
|
2646
|
+
---
|
|
2647
|
+
|
|
2648
|
+
## 27. Resources & Links
|
|
2649
|
+
|
|
2650
|
+
### Official Documentation
|
|
2651
|
+
- [MCP Specification](https://spec.modelcontextprotocol.io/)
|
|
2652
|
+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
2653
|
+
- [Library GitHub Repository](https://github.com/your-org/mcp-http-webhook)
|
|
2654
|
+
- [API Documentation](https://docs.mcp-http-webhook.dev/api)
|
|
2655
|
+
|
|
2656
|
+
### Community
|
|
2657
|
+
- [Discord Server](https://discord.gg/mcp-webhook)
|
|
2658
|
+
- [GitHub Discussions](https://github.com/your-org/mcp-http-webhook/discussions)
|
|
2659
|
+
- [Stack Overflow Tag: mcp-http-webhook](https://stackoverflow.com/questions/tagged/mcp-http-webhook)
|
|
2660
|
+
|
|
2661
|
+
### Examples & Tutorials
|
|
2662
|
+
- [Complete Examples Repository](https://github.com/your-org/mcp-http-webhook/tree/main/examples)
|
|
2663
|
+
- [Video Tutorials](https://youtube.com/playlist?list=xxx)
|
|
2664
|
+
- [Blog: Building Your First MCP Server](https://blog.example.com/first-mcp-server)
|
|
2665
|
+
- [Blog: Scaling MCP to 1M Subscriptions](https://blog.example.com/scaling-mcp)
|
|
2666
|
+
|
|
2667
|
+
### Related Projects
|
|
2668
|
+
- [MCP Client Libraries](https://github.com/modelcontextprotocol)
|
|
2669
|
+
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
|
|
2670
|
+
- [MCP CLI Tools](https://github.com/modelcontextprotocol/cli)
|
|
2671
|
+
|
|
2672
|
+
### Third-Party Integrations
|
|
2673
|
+
- [GitHub Webhooks Documentation](https://docs.github.com/en/webhooks)
|
|
2674
|
+
- [Google Drive Push Notifications](https://developers.google.com/drive/api/guides/push)
|
|
2675
|
+
- [Slack Events API](https://api.slack.com/apis/connections/events-api)
|
|
2676
|
+
- [Stripe Webhooks](https://stripe.com/docs/webhooks)
|
|
2677
|
+
- [PostgreSQL NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html)
|
|
2678
|
+
|
|
2679
|
+
---
|
|
2680
|
+
|
|
2681
|
+
## 28. License
|
|
2682
|
+
|
|
2683
|
+
**MIT License**
|
|
2684
|
+
|
|
2685
|
+
Copyright (c) 2025 Your Organization
|
|
2686
|
+
|
|
2687
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
2688
|
+
|
|
2689
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
2690
|
+
|
|
2691
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2692
|
+
|
|
2693
|
+
---
|
|
2694
|
+
|
|
2695
|
+
## 29. Support
|
|
2696
|
+
|
|
2697
|
+
### Commercial Support
|
|
2698
|
+
|
|
2699
|
+
For enterprise support, custom integrations, and consulting services:
|
|
2700
|
+
- Email: support@mcp-http-webhook.dev
|
|
2701
|
+
- Enterprise Plans: [View Pricing](https://mcp-http-webhook.dev/pricing)
|
|
2702
|
+
|
|
2703
|
+
### Community Support
|
|
2704
|
+
|
|
2705
|
+
- **GitHub Issues**: Bug reports and feature requests
|
|
2706
|
+
- **Discussions**: Questions and community help
|
|
2707
|
+
- **Discord**: Real-time chat with maintainers and community
|
|
2708
|
+
- **Stack Overflow**: Tag your questions with `mcp-http-webhook`
|
|
2709
|
+
|
|
2710
|
+
### Bug Reports
|
|
2711
|
+
|
|
2712
|
+
When filing a bug report, please include:
|
|
2713
|
+
|
|
2714
|
+
```markdown
|
|
2715
|
+
**Environment:**
|
|
2716
|
+
- Library version: 1.0.0
|
|
2717
|
+
- Node.js version: 20.x
|
|
2718
|
+
- Store implementation: Redis 7.x
|
|
2719
|
+
- Deployment: Kubernetes / Docker / Bare metal
|
|
2720
|
+
|
|
2721
|
+
**Expected Behavior:**
|
|
2722
|
+
[What you expected to happen]
|
|
2723
|
+
|
|
2724
|
+
**Actual Behavior:**
|
|
2725
|
+
[What actually happened]
|
|
2726
|
+
|
|
2727
|
+
**Reproduction Steps:**
|
|
2728
|
+
1. Create server with config...
|
|
2729
|
+
2. Subscribe to resource...
|
|
2730
|
+
3. Send webhook...
|
|
2731
|
+
|
|
2732
|
+
**Logs:**
|
|
2733
|
+
```
|
|
2734
|
+
[Relevant log output]
|
|
2735
|
+
```
|
|
2736
|
+
|
|
2737
|
+
**Additional Context:**
|
|
2738
|
+
[Any other relevant information]
|
|
2739
|
+
```
|
|
2740
|
+
|
|
2741
|
+
---
|
|
2742
|
+
|
|
2743
|
+
## 30. Acknowledgments
|
|
2744
|
+
|
|
2745
|
+
This library builds upon the excellent work of:
|
|
2746
|
+
|
|
2747
|
+
- **Anthropic** - For creating the Model Context Protocol specification
|
|
2748
|
+
- **MCP Community** - For feedback and contributions
|
|
2749
|
+
- **Third-Party Service Providers** - For comprehensive webhook documentation
|
|
2750
|
+
- **Open Source Contributors** - For example implementations and testing
|
|
2751
|
+
|
|
2752
|
+
Special thanks to all contributors who have helped make this library possible.
|
|
2753
|
+
|
|
2754
|
+
---
|
|
2755
|
+
|
|
2756
|
+
## Appendix A: Complete Type Definitions
|
|
2757
|
+
|
|
2758
|
+
See [Type Reference Documentation](https://docs.mcp-http-webhook.dev/types) for complete TypeScript type definitions.
|
|
2759
|
+
|
|
2760
|
+
## Appendix B: HTTP API Specification
|
|
2761
|
+
|
|
2762
|
+
See [HTTP API Reference](https://docs.mcp-http-webhook.dev/http-api) for detailed endpoint specifications, request/response formats, and error codes.
|
|
2763
|
+
|
|
2764
|
+
## Appendix C: Storage Migration Guide
|
|
2765
|
+
|
|
2766
|
+
See [Storage Migration Guide](https://docs.mcp-http-webhook.dev/storage-migration) for instructions on migrating between different key-value store implementations.
|
|
2767
|
+
|
|
2768
|
+
## Appendix D: Performance Benchmarks
|
|
2769
|
+
|
|
2770
|
+
See [Benchmark Results](https://docs.mcp-http-webhook.dev/benchmarks) for performance comparisons across different configurations and deployment scenarios.
|
|
2771
|
+
|
|
2772
|
+
---
|
|
2773
|
+
|
|
2774
|
+
**Document Version:** 1.0.0
|
|
2775
|
+
**Last Updated:** 2025-01-15
|
|
2776
|
+
**Maintained By:** MCP HTTP Webhook Team
|
|
2777
|
+
|
|
2778
|
+
For the latest version of this document, visit: [https://docs.mcp-http-webhook.dev/specification](https://docs.mcp-http-webhook.dev/specification)
|