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.
Files changed (80) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.prettierrc.json +8 -0
  3. package/ARCHITECTURE.md +269 -0
  4. package/CONTRIBUTING.md +136 -0
  5. package/GETTING_STARTED.md +310 -0
  6. package/IMPLEMENTATION.md +294 -0
  7. package/LICENSE +21 -0
  8. package/MIGRATION_TO_SDK.md +263 -0
  9. package/README.md +496 -0
  10. package/SDK_INTEGRATION_COMPLETE.md +300 -0
  11. package/STANDARD_SUBSCRIPTIONS.md +268 -0
  12. package/STANDARD_SUBSCRIPTIONS_COMPLETE.md +309 -0
  13. package/SUMMARY.md +272 -0
  14. package/Spec.md +2778 -0
  15. package/dist/errors/index.d.ts +52 -0
  16. package/dist/errors/index.d.ts.map +1 -0
  17. package/dist/errors/index.js +81 -0
  18. package/dist/errors/index.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +37 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/protocol/ProtocolHandler.d.ts +37 -0
  24. package/dist/protocol/ProtocolHandler.d.ts.map +1 -0
  25. package/dist/protocol/ProtocolHandler.js +172 -0
  26. package/dist/protocol/ProtocolHandler.js.map +1 -0
  27. package/dist/server.d.ts +6 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +502 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/stores/InMemoryStore.d.ts +27 -0
  32. package/dist/stores/InMemoryStore.d.ts.map +1 -0
  33. package/dist/stores/InMemoryStore.js +73 -0
  34. package/dist/stores/InMemoryStore.js.map +1 -0
  35. package/dist/stores/RedisStore.d.ts +18 -0
  36. package/dist/stores/RedisStore.d.ts.map +1 -0
  37. package/dist/stores/RedisStore.js +45 -0
  38. package/dist/stores/RedisStore.js.map +1 -0
  39. package/dist/stores/index.d.ts +3 -0
  40. package/dist/stores/index.d.ts.map +1 -0
  41. package/dist/stores/index.js +9 -0
  42. package/dist/stores/index.js.map +1 -0
  43. package/dist/subscriptions/SubscriptionManager.d.ts +49 -0
  44. package/dist/subscriptions/SubscriptionManager.d.ts.map +1 -0
  45. package/dist/subscriptions/SubscriptionManager.js +181 -0
  46. package/dist/subscriptions/SubscriptionManager.js.map +1 -0
  47. package/dist/types/index.d.ts +271 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/types/index.js +16 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/dist/utils/index.d.ts +51 -0
  52. package/dist/utils/index.d.ts.map +1 -0
  53. package/dist/utils/index.js +154 -0
  54. package/dist/utils/index.js.map +1 -0
  55. package/dist/webhooks/WebhookManager.d.ts +27 -0
  56. package/dist/webhooks/WebhookManager.d.ts.map +1 -0
  57. package/dist/webhooks/WebhookManager.js +174 -0
  58. package/dist/webhooks/WebhookManager.js.map +1 -0
  59. package/examples/GITHUB_LIVE_EXAMPLE.md +308 -0
  60. package/examples/GITHUB_LIVE_SETUP.md +253 -0
  61. package/examples/QUICKSTART.md +130 -0
  62. package/examples/basic-setup.ts +142 -0
  63. package/examples/github-server-live.ts +690 -0
  64. package/examples/github-server.ts +223 -0
  65. package/examples/google-drive-server-live.ts +773 -0
  66. package/examples/start-github-live.sh +53 -0
  67. package/jest.config.js +20 -0
  68. package/package.json +58 -0
  69. package/src/errors/index.ts +81 -0
  70. package/src/index.ts +19 -0
  71. package/src/server.ts +595 -0
  72. package/src/stores/InMemoryStore.ts +87 -0
  73. package/src/stores/RedisStore.ts +51 -0
  74. package/src/stores/index.ts +2 -0
  75. package/src/subscriptions/SubscriptionManager.ts +240 -0
  76. package/src/types/index.ts +341 -0
  77. package/src/utils/index.ts +156 -0
  78. package/src/webhooks/WebhookManager.ts +230 -0
  79. package/test-sdk-integration.sh +157 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,156 @@
1
+ import crypto from 'crypto';
2
+ import { nanoid } from 'nanoid';
3
+
4
+ /**
5
+ * Generate a unique subscription ID
6
+ */
7
+ export function generateSubscriptionId(): string {
8
+ return `sub_${nanoid(16)}`;
9
+ }
10
+
11
+ /**
12
+ * Generate webhook URL for third-party services
13
+ */
14
+ export function generateWebhookUrl(publicUrl: string, subscriptionId: string): string {
15
+ const baseUrl = publicUrl.endsWith('/') ? publicUrl.slice(0, -1) : publicUrl;
16
+ return `${baseUrl}/webhooks/incoming/${subscriptionId}`;
17
+ }
18
+
19
+ /**
20
+ * Parse URI template
21
+ * Example: parseUriTemplate('github://repo/{owner}/{repo}', 'github://repo/octocat/hello')
22
+ * Returns: { owner: 'octocat', repo: 'hello' }
23
+ */
24
+ export function parseUriTemplate(template: string, uri: string): Record<string, string> {
25
+ const templateParts = template.split('/');
26
+ const uriParts = uri.split('/');
27
+
28
+ if (templateParts.length !== uriParts.length) {
29
+ throw new Error(`URI ${uri} does not match template ${template}`);
30
+ }
31
+
32
+ const params: Record<string, string> = {};
33
+
34
+ for (let i = 0; i < templateParts.length; i++) {
35
+ const templatePart = templateParts[i];
36
+ const uriPart = uriParts[i];
37
+
38
+ if (templatePart.startsWith('{') && templatePart.endsWith('}')) {
39
+ const paramName = templatePart.slice(1, -1);
40
+ params[paramName] = uriPart;
41
+ } else if (templatePart !== uriPart) {
42
+ throw new Error(`URI ${uri} does not match template ${template}`);
43
+ }
44
+ }
45
+
46
+ return params;
47
+ }
48
+
49
+ /**
50
+ * Match URI against template
51
+ */
52
+ export function matchUriTemplate(template: string, uri: string): boolean {
53
+ try {
54
+ parseUriTemplate(template, uri);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Create HMAC signature
63
+ */
64
+ export function createHmacSignature(
65
+ payload: any,
66
+ secret: string,
67
+ algorithm: 'sha256' | 'sha1' = 'sha256'
68
+ ): string {
69
+ const hmac = crypto.createHmac(algorithm, secret);
70
+ const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
71
+ hmac.update(data);
72
+ return `${algorithm}=${hmac.digest('hex')}`;
73
+ }
74
+
75
+ /**
76
+ * Verify HMAC signature (timing-safe comparison)
77
+ */
78
+ export function verifyHmacSignature(
79
+ payload: any,
80
+ signature: string,
81
+ secret: string,
82
+ algorithm: 'sha256' | 'sha1' = 'sha256'
83
+ ): boolean {
84
+ try {
85
+ const expected = createHmacSignature(payload, secret, algorithm);
86
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Sleep utility for retry delays
94
+ */
95
+ export function sleep(ms: number): Promise<void> {
96
+ return new Promise((resolve) => setTimeout(resolve, ms));
97
+ }
98
+
99
+ /**
100
+ * Exponential backoff calculator
101
+ */
102
+ export function calculateBackoff(attempt: number, baseDelay: number): number {
103
+ return baseDelay * Math.pow(2, attempt);
104
+ }
105
+
106
+ /**
107
+ * Sanitize URL for logging (remove sensitive query params)
108
+ */
109
+ export function sanitizeUrl(url: string): string {
110
+ try {
111
+ const urlObj = new URL(url);
112
+ urlObj.searchParams.delete('token');
113
+ urlObj.searchParams.delete('secret');
114
+ urlObj.searchParams.delete('api_key');
115
+ return urlObj.toString();
116
+ } catch {
117
+ return url;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Extract error message safely
123
+ */
124
+ export function extractErrorMessage(error: unknown): string {
125
+ if (error instanceof Error) {
126
+ return error.message;
127
+ }
128
+ if (typeof error === 'string') {
129
+ return error;
130
+ }
131
+ return 'Unknown error';
132
+ }
133
+
134
+ /**
135
+ * Validate URL format
136
+ */
137
+ export function isValidUrl(url: string): boolean {
138
+ try {
139
+ new URL(url);
140
+ return true;
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Validate HTTPS URL
148
+ */
149
+ export function isHttpsUrl(url: string): boolean {
150
+ try {
151
+ const urlObj = new URL(url);
152
+ return urlObj.protocol === 'https:';
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
@@ -0,0 +1,230 @@
1
+ import {
2
+ WebhookConfig,
3
+ StoredSubscription,
4
+ ResourceDefinition,
5
+ WebhookChangeInfo,
6
+ Logger,
7
+ } from '../types';
8
+ import { WebhookError } from '../errors';
9
+ import { sleep, calculateBackoff, extractErrorMessage } from '../utils';
10
+
11
+ export class WebhookManager {
12
+ private readonly timeout: number;
13
+ private readonly retries: number;
14
+ private readonly retryDelay: number;
15
+
16
+ constructor(
17
+ private config: WebhookConfig | undefined,
18
+ private resources: ResourceDefinition[],
19
+ private logger?: Logger
20
+ ) {
21
+ this.timeout = config?.outgoing?.timeout ?? 5000;
22
+ this.retries = config?.outgoing?.retries ?? 3;
23
+ this.retryDelay = config?.outgoing?.retryDelay ?? 1000;
24
+ }
25
+
26
+ /**
27
+ * Process incoming webhook from third-party service
28
+ */
29
+ async processIncomingWebhook(
30
+ subscriptionId: string,
31
+ payload: any,
32
+ headers: Record<string, string>,
33
+ subscription: StoredSubscription
34
+ ): Promise<WebhookChangeInfo | null> {
35
+ this.logger?.debug('Processing incoming webhook', {
36
+ subscriptionId,
37
+ resourceType: subscription.resourceType,
38
+ });
39
+
40
+ // Find resource definition
41
+ const resource = this.findResourceByName(subscription.resourceType);
42
+ if (!resource?.subscription) {
43
+ throw new WebhookError('Resource subscription handler not found');
44
+ }
45
+
46
+ // Verify signature if configured
47
+ if (this.config?.verifyIncomingSignature) {
48
+ const signature = headers['x-hub-signature-256'] || headers['x-signature'];
49
+ if (signature && this.config.incomingSecret) {
50
+ const isValid = this.config.verifyIncomingSignature(
51
+ payload,
52
+ signature,
53
+ this.config.incomingSecret
54
+ );
55
+
56
+ if (!isValid) {
57
+ this.logger?.warn('Invalid webhook signature', { subscriptionId });
58
+ throw new WebhookError('Invalid webhook signature');
59
+ }
60
+ }
61
+ }
62
+
63
+ try {
64
+ // Call resource's onWebhook handler
65
+ const changeInfo = await resource.subscription.onWebhook(subscriptionId, payload, headers);
66
+
67
+ this.logger?.debug('Webhook processed', {
68
+ subscriptionId,
69
+ changeType: changeInfo?.changeType,
70
+ });
71
+
72
+ return changeInfo;
73
+ } catch (error) {
74
+ this.logger?.error('Failed to process webhook', {
75
+ subscriptionId,
76
+ error: extractErrorMessage(error),
77
+ });
78
+ throw new WebhookError('Failed to process webhook', { cause: error });
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Send notification to client webhook in standard MCP format
84
+ */
85
+ async notifyClient(
86
+ subscription: StoredSubscription,
87
+ changeInfo: WebhookChangeInfo
88
+ ): Promise<void> {
89
+ // Standard MCP notification format
90
+ const payload = {
91
+ jsonrpc: '2.0',
92
+ method: 'notifications/resources/updated',
93
+ params: {
94
+ uri: changeInfo.resourceUri,
95
+ title: changeInfo.data?.title,
96
+ _meta: {
97
+ changeType: changeInfo.changeType,
98
+ subscriptionId: subscription.uri,
99
+ timestamp: new Date().toISOString(),
100
+ ...changeInfo.data
101
+ }
102
+ }
103
+ };
104
+
105
+ this.logger?.info('Notifying client (MCP format)', {
106
+ url: subscription.clientCallbackUrl,
107
+ uri: changeInfo.resourceUri,
108
+ changeType: changeInfo.changeType,
109
+ });
110
+
111
+ // Sign payload if secret is provided
112
+ let signature: string | undefined;
113
+ if (subscription.clientCallbackSecret && this.config?.outgoing?.signPayload) {
114
+ signature = this.config.outgoing.signPayload(payload, subscription.clientCallbackSecret);
115
+ }
116
+
117
+ // Call before hook
118
+ this.config?.outgoing?.onBeforeCall?.(subscription.clientCallbackUrl, payload);
119
+
120
+ // Attempt delivery with retries
121
+ let lastError: Error | undefined;
122
+
123
+ for (let attempt = 0; attempt < this.retries; attempt++) {
124
+ try {
125
+ const response = await this.callWebhook(
126
+ subscription.clientCallbackUrl,
127
+ payload,
128
+ signature
129
+ );
130
+
131
+ // Call after hook
132
+ this.config?.outgoing?.onAfterCall?.(subscription.clientCallbackUrl, response);
133
+
134
+ this.logger?.info('Client notified successfully (MCP format)', {
135
+ url: subscription.clientCallbackUrl,
136
+ attempt: attempt + 1,
137
+ });
138
+
139
+ return; // Success
140
+ } catch (error) {
141
+ lastError = error instanceof Error ? error : new Error(String(error));
142
+
143
+ this.logger?.warn('Webhook delivery failed', {
144
+ url: subscription.clientCallbackUrl,
145
+ attempt: attempt + 1,
146
+ error: extractErrorMessage(error),
147
+ });
148
+
149
+ // Don't retry on client errors (4xx)
150
+ if (error instanceof Error && 'status' in error) {
151
+ const status = (error as any).status;
152
+ if (status >= 400 && status < 500) {
153
+ throw new WebhookError('Client error, not retrying', { status });
154
+ }
155
+ }
156
+
157
+ // Wait before retry (exponential backoff)
158
+ if (attempt < this.retries - 1) {
159
+ const delay = calculateBackoff(attempt, this.retryDelay);
160
+ await sleep(delay);
161
+ }
162
+ }
163
+ }
164
+
165
+ // All retries failed
166
+ this.config?.outgoing?.onAfterCall?.(subscription.clientCallbackUrl, null, lastError);
167
+
168
+ throw new WebhookError('Failed to notify client after all retries', {
169
+ attempts: this.retries,
170
+ lastError: extractErrorMessage(lastError),
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Call webhook URL
176
+ */
177
+ private async callWebhook(
178
+ url: string,
179
+ payload: any,
180
+ signature?: string
181
+ ): Promise<any> {
182
+ const controller = new AbortController();
183
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
184
+
185
+ try {
186
+ const headers: Record<string, string> = {
187
+ 'Content-Type': 'application/json',
188
+ };
189
+
190
+ if (signature) {
191
+ headers['X-MCP-Signature'] = signature;
192
+ }
193
+
194
+ const response = await fetch(url, {
195
+ method: 'POST',
196
+ headers,
197
+ body: JSON.stringify(payload),
198
+ signal: controller.signal,
199
+ });
200
+
201
+ clearTimeout(timeoutId);
202
+
203
+ if (!response.ok) {
204
+ const error: any = new Error(`HTTP ${response.status}: ${response.statusText}`);
205
+ error.status = response.status;
206
+ throw error;
207
+ }
208
+
209
+ return {
210
+ ok: true,
211
+ status: response.status,
212
+ };
213
+ } catch (error) {
214
+ clearTimeout(timeoutId);
215
+
216
+ if (error instanceof Error && error.name === 'AbortError') {
217
+ throw new Error(`Request timeout after ${this.timeout}ms`);
218
+ }
219
+
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Find resource by name
226
+ */
227
+ private findResourceByName(name: string): ResourceDefinition | undefined {
228
+ return this.resources.find((r) => r.name === name);
229
+ }
230
+ }
@@ -0,0 +1,157 @@
1
+ #!/bin/bash
2
+
3
+ # Test MCP SDK Integration
4
+ # This script verifies the server works with standard MCP clients
5
+
6
+ set -e
7
+
8
+ echo "๐Ÿงช Testing MCP SDK Integration..."
9
+ echo ""
10
+
11
+ # Build the project
12
+ echo "๐Ÿ“ฆ Building project..."
13
+ npm run build
14
+ echo "โœ… Build successful"
15
+ echo ""
16
+
17
+ # Start the server in background
18
+ echo "๐Ÿš€ Starting server..."
19
+ node dist/examples/basic-setup.js &
20
+ SERVER_PID=$!
21
+
22
+ # Wait for server to start
23
+ sleep 3
24
+
25
+ # Function to cleanup
26
+ cleanup() {
27
+ echo ""
28
+ echo "๐Ÿงน Cleaning up..."
29
+ kill $SERVER_PID 2>/dev/null || true
30
+ }
31
+ trap cleanup EXIT
32
+
33
+ echo "โœ… Server started (PID: $SERVER_PID)"
34
+ echo ""
35
+
36
+ # Test 1: Health check
37
+ echo "Test 1: Health check"
38
+ RESPONSE=$(curl -s http://localhost:3000/health)
39
+ if echo "$RESPONSE" | grep -q "ok"; then
40
+ echo "โœ… Health check passed"
41
+ else
42
+ echo "โŒ Health check failed: $RESPONSE"
43
+ exit 1
44
+ fi
45
+ echo ""
46
+
47
+ # Test 2: Standard MCP endpoint - List tools
48
+ echo "Test 2: List tools via standard MCP endpoint"
49
+ RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
50
+ -H "Content-Type: application/json" \
51
+ -d '{
52
+ "jsonrpc": "2.0",
53
+ "id": 1,
54
+ "method": "tools/list"
55
+ }')
56
+
57
+ if echo "$RESPONSE" | grep -q "echo"; then
58
+ echo "โœ… Tools list successful"
59
+ echo " Found tools: $(echo "$RESPONSE" | grep -o '"name":"[^"]*"' | cut -d'"' -f4 | tr '\n' ', ')"
60
+ else
61
+ echo "โŒ Tools list failed: $RESPONSE"
62
+ exit 1
63
+ fi
64
+ echo ""
65
+
66
+ # Test 3: Call a tool
67
+ echo "Test 3: Call echo tool"
68
+ RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
69
+ -H "Content-Type: application/json" \
70
+ -d '{
71
+ "jsonrpc": "2.0",
72
+ "id": 2,
73
+ "method": "tools/call",
74
+ "params": {
75
+ "name": "echo",
76
+ "arguments": {
77
+ "message": "Hello from MCP SDK!"
78
+ }
79
+ }
80
+ }')
81
+
82
+ if echo "$RESPONSE" | grep -q "Hello from MCP SDK"; then
83
+ echo "โœ… Tool call successful"
84
+ else
85
+ echo "โŒ Tool call failed: $RESPONSE"
86
+ exit 1
87
+ fi
88
+ echo ""
89
+
90
+ # Test 4: List resources
91
+ echo "Test 4: List resources"
92
+ RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
93
+ -H "Content-Type: application/json" \
94
+ -d '{
95
+ "jsonrpc": "2.0",
96
+ "id": 3,
97
+ "method": "resources/list"
98
+ }')
99
+
100
+ if echo "$RESPONSE" | grep -q "Greeting Resource"; then
101
+ echo "โœ… Resources list successful"
102
+ else
103
+ echo "โŒ Resources list failed: $RESPONSE"
104
+ exit 1
105
+ fi
106
+ echo ""
107
+
108
+ # Test 5: Read a resource
109
+ echo "Test 5: Read a resource"
110
+ RESPONSE=$(curl -s -X POST http://localhost:3000/mcp \
111
+ -H "Content-Type: application/json" \
112
+ -d '{
113
+ "jsonrpc": "2.0",
114
+ "id": 4,
115
+ "method": "resources/read",
116
+ "params": {
117
+ "uri": "example://greeting/world"
118
+ }
119
+ }')
120
+
121
+ if echo "$RESPONSE" | grep -q "Hello"; then
122
+ echo "โœ… Resource read successful"
123
+ else
124
+ echo "โŒ Resource read failed: $RESPONSE"
125
+ exit 1
126
+ fi
127
+ echo ""
128
+
129
+ # Test 6: Webhook subscription endpoint (extension)
130
+ echo "Test 6: Webhook subscription endpoint (extension)"
131
+ RESPONSE=$(curl -s -X POST http://localhost:3000/mcp/resources/subscribe \
132
+ -H "Content-Type: application/json" \
133
+ -d '{
134
+ "uri": "example://greeting/alice",
135
+ "callbackUrl": "https://example.com/webhook",
136
+ "callbackSecret": "test-secret"
137
+ }')
138
+
139
+ if echo "$RESPONSE" | grep -q "subscriptionId"; then
140
+ echo "โœ… Webhook subscription successful"
141
+ SUBSCRIPTION_ID=$(echo "$RESPONSE" | grep -o '"subscriptionId":"[^"]*"' | cut -d'"' -f4)
142
+ echo " Subscription ID: $SUBSCRIPTION_ID"
143
+ else
144
+ echo "โŒ Webhook subscription failed: $RESPONSE"
145
+ exit 1
146
+ fi
147
+ echo ""
148
+
149
+ echo "๐ŸŽ‰ All tests passed!"
150
+ echo ""
151
+ echo "โœจ MCP SDK Integration is working correctly!"
152
+ echo ""
153
+ echo "You can now:"
154
+ echo " - Use with MCP Inspector: npx @modelcontextprotocol/inspector http://localhost:3000/mcp"
155
+ echo " - Configure Claude Desktop to use: http://localhost:3000/mcp"
156
+ echo " - Integrate with any standard MCP client"
157
+ echo ""
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "moduleResolution": "node",
17
+ "types": ["node", "jest"]
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
21
+ }