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,51 @@
1
+ import { KeyValueStore } from '../types';
2
+
3
+ /**
4
+ * Redis store implementation
5
+ * Requires ioredis package
6
+ */
7
+ export class RedisStore implements KeyValueStore {
8
+ constructor(private redis: any) {} // ioredis instance
9
+
10
+ async get(key: string): Promise<string | null> {
11
+ return await this.redis.get(key);
12
+ }
13
+
14
+ async set(key: string, value: string, ttl?: number): Promise<void> {
15
+ if (ttl) {
16
+ await this.redis.setex(key, ttl, value);
17
+ } else {
18
+ await this.redis.set(key, value);
19
+ }
20
+ }
21
+
22
+ async delete(key: string): Promise<void> {
23
+ await this.redis.del(key);
24
+ }
25
+
26
+ async scan(pattern: string): Promise<string[]> {
27
+ const keys: string[] = [];
28
+ let cursor = '0';
29
+
30
+ do {
31
+ const [newCursor, matches] = await this.redis.scan(
32
+ cursor,
33
+ 'MATCH',
34
+ pattern,
35
+ 'COUNT',
36
+ 100
37
+ );
38
+ cursor = newCursor;
39
+ keys.push(...matches);
40
+ } while (cursor !== '0');
41
+
42
+ return keys;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Create Redis store from connection URL
48
+ */
49
+ export function createRedisStore(redis: any): KeyValueStore {
50
+ return new RedisStore(redis);
51
+ }
@@ -0,0 +1,2 @@
1
+ export { InMemoryStore } from './InMemoryStore';
2
+ export { RedisStore, createRedisStore } from './RedisStore';
@@ -0,0 +1,240 @@
1
+ import { StorageError, ValidationError } from '../errors';
2
+ import {
3
+ AuthContext,
4
+ KeyValueStore,
5
+ Logger,
6
+ ResourceDefinition,
7
+ StoredSubscription
8
+ } from '../types';
9
+ import { generateSubscriptionId, generateWebhookUrl } from '../utils';
10
+
11
+ export class SubscriptionManager {
12
+ constructor(
13
+ private store: KeyValueStore,
14
+ private resources: ResourceDefinition[],
15
+ private publicUrl: string,
16
+ private logger?: Logger
17
+ ) {}
18
+
19
+ /**
20
+ * Create a new subscription
21
+ */
22
+ async createSubscription(params: {
23
+ uri: string;
24
+ clientCallbackUrl: string;
25
+ clientCallbackSecret?: string;
26
+ context: AuthContext;
27
+ }): Promise<{ subscriptionId: string; status: string }> {
28
+ const { uri, clientCallbackUrl, clientCallbackSecret, context } = params;
29
+
30
+ // Find resource definition
31
+ const resource = this.findResourceForUri(uri);
32
+ if (!resource) {
33
+ throw new ValidationError(`No resource found for URI: ${uri}`);
34
+ }
35
+
36
+ if (!resource.subscription) {
37
+ throw new ValidationError(`Resource ${resource.name} does not support subscriptions`);
38
+ }
39
+
40
+ // Generate subscription ID
41
+ const subscriptionId = generateSubscriptionId();
42
+ const thirdPartyWebhookUrl = generateWebhookUrl(this.publicUrl, subscriptionId);
43
+
44
+ this.logger?.info('Creating subscription', {
45
+ subscriptionId,
46
+ uri,
47
+ clientCallbackUrl,
48
+ });
49
+
50
+ try {
51
+ // Call resource's onSubscribe handler
52
+ const metadata = await resource.subscription.onSubscribe(
53
+ uri,
54
+ subscriptionId,
55
+ thirdPartyWebhookUrl,
56
+ context
57
+ );
58
+
59
+ // Store subscription data
60
+ const subscriptionData: StoredSubscription = {
61
+ uri,
62
+ resourceType: resource.name,
63
+ clientCallbackUrl,
64
+ clientCallbackSecret,
65
+ userId: context.userId,
66
+ thirdPartyWebhookId: metadata.thirdPartyWebhookId,
67
+ metadata: metadata.metadata,
68
+ createdAt: Date.now(),
69
+ };
70
+
71
+ await this.storeSubscription(subscriptionId, subscriptionData, context.userId);
72
+
73
+ this.logger?.info('Subscription created', { subscriptionId });
74
+
75
+ return {
76
+ subscriptionId,
77
+ status: 'active',
78
+ };
79
+ } catch (error) {
80
+ this.logger?.error('Failed to create subscription', {
81
+ subscriptionId,
82
+ error: error instanceof Error ? error.message : String(error),
83
+ });
84
+ throw new StorageError('Failed to create subscription', { cause: error });
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Delete a subscription
90
+ */
91
+ async deleteSubscription(subscriptionId: string, context: AuthContext): Promise<void> {
92
+ this.logger?.info('Deleting subscription', { subscriptionId });
93
+
94
+ // Load subscription
95
+ const subscription = await this.loadSubscription(subscriptionId);
96
+ if (!subscription) {
97
+ throw new ValidationError(`Subscription ${subscriptionId} not found`);
98
+ }
99
+
100
+ // Verify ownership
101
+ if (subscription.userId !== context.userId) {
102
+ throw new ValidationError('Unauthorized to delete this subscription');
103
+ }
104
+
105
+ // Find resource
106
+ const resource = this.findResourceForUri(subscription.uri);
107
+ if (!resource?.subscription) {
108
+ throw new StorageError('Resource subscription configuration not found');
109
+ }
110
+
111
+ try {
112
+ // Call resource's onUnsubscribe handler
113
+ await resource.subscription.onUnsubscribe(
114
+ subscription.uri,
115
+ subscriptionId,
116
+ {
117
+ thirdPartyWebhookId: subscription.thirdPartyWebhookId,
118
+ metadata: subscription.metadata,
119
+ },
120
+ context
121
+ );
122
+
123
+ // Delete from store
124
+ await this.removeSubscription(subscriptionId, context.userId);
125
+
126
+ this.logger?.info('Subscription deleted', { subscriptionId });
127
+ } catch (error) {
128
+ this.logger?.error('Failed to delete subscription', {
129
+ subscriptionId,
130
+ error: error instanceof Error ? error.message : String(error),
131
+ });
132
+ throw new StorageError('Failed to delete subscription', { cause: error });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get subscription by ID
138
+ */
139
+ async getSubscription(subscriptionId: string): Promise<StoredSubscription | null> {
140
+ return this.loadSubscription(subscriptionId);
141
+ }
142
+
143
+ /**
144
+ * List subscriptions for a user
145
+ */
146
+ async listSubscriptions(userId: string): Promise<StoredSubscription[]> {
147
+ const indexKey = `user:${userId}:subscriptions`;
148
+ const indexData = await this.store.get(indexKey);
149
+
150
+ if (!indexData) {
151
+ return [];
152
+ }
153
+
154
+ const subscriptionIds: string[] = JSON.parse(indexData);
155
+
156
+ const subscriptions = await Promise.all(
157
+ subscriptionIds.map((id) => this.loadSubscription(id))
158
+ );
159
+
160
+ return subscriptions.filter((s): s is StoredSubscription => s !== null);
161
+ }
162
+
163
+ /**
164
+ * Store subscription data
165
+ */
166
+ private async storeSubscription(
167
+ subscriptionId: string,
168
+ data: StoredSubscription,
169
+ userId: string
170
+ ): Promise<void> {
171
+ const key = `subscription:${subscriptionId}`;
172
+ await this.store.set(key, JSON.stringify(data));
173
+
174
+ // Update user index
175
+ const indexKey = `user:${userId}:subscriptions`;
176
+ const existing = await this.store.get(indexKey);
177
+ const subscriptionIds: string[] = existing ? JSON.parse(existing) : [];
178
+
179
+ if (!subscriptionIds.includes(subscriptionId)) {
180
+ subscriptionIds.push(subscriptionId);
181
+ await this.store.set(indexKey, JSON.stringify(subscriptionIds));
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Load subscription data
187
+ */
188
+ private async loadSubscription(subscriptionId: string): Promise<StoredSubscription | null> {
189
+ const key = `subscription:${subscriptionId}`;
190
+ const data = await this.store.get(key);
191
+
192
+ if (!data) {
193
+ return null;
194
+ }
195
+
196
+ return JSON.parse(data);
197
+ }
198
+
199
+ /**
200
+ * Remove subscription data
201
+ */
202
+ private async removeSubscription(subscriptionId: string, userId: string): Promise<void> {
203
+ const key = `subscription:${subscriptionId}`;
204
+ await this.store.delete(key);
205
+
206
+ // Update user index
207
+ const indexKey = `user:${userId}:subscriptions`;
208
+ const existing = await this.store.get(indexKey);
209
+
210
+ if (existing) {
211
+ const subscriptionIds: string[] = JSON.parse(existing);
212
+ const filtered = subscriptionIds.filter((id) => id !== subscriptionId);
213
+ await this.store.set(indexKey, JSON.stringify(filtered));
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Find resource definition for URI
219
+ */
220
+ private findResourceForUri(uri: string): ResourceDefinition | undefined {
221
+ return this.resources.find((resource) => {
222
+ // Convert URI template to regex pattern
223
+ // Step 1: Replace template variables with a placeholder
224
+ const withPlaceholders = resource.uri.replace(/\{[^}]+\}/g, '__PLACEHOLDER__');
225
+
226
+ // Step 2: Escape special regex characters (but not our placeholders)
227
+ const escapedPattern = withPlaceholders.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
228
+
229
+ // Step 3: Replace placeholders with capture groups
230
+ const patternString = escapedPattern.replace(/__PLACEHOLDER__/g, '([^/]+)');
231
+
232
+ // Step 4: Create regex with anchors
233
+ const pattern = new RegExp('^' + patternString + '$');
234
+
235
+ console.log('Pattern for resource', resource.name, ':', pattern);
236
+ console.log('Testing URI:', uri);
237
+ return pattern.test(uri);
238
+ });
239
+ }
240
+ }
@@ -0,0 +1,341 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ /**
4
+ * JSON Schema definition for input validation
5
+ */
6
+ export interface JSONSchema {
7
+ type: string;
8
+ properties?: Record<string, any>;
9
+ required?: string[];
10
+ [key: string]: any;
11
+ }
12
+
13
+ /**
14
+ * Authentication context passed to handlers
15
+ */
16
+ export interface AuthContext {
17
+ userId: string;
18
+ [key: string]: any;
19
+ }
20
+
21
+ /**
22
+ * Authentication handler function
23
+ */
24
+ export type AuthenticateFunction = (req: Request) => Promise<AuthContext>;
25
+
26
+ /**
27
+ * Logger interface
28
+ */
29
+ export interface Logger {
30
+ debug(message: string, meta?: any): void;
31
+ info(message: string, meta?: any): void;
32
+ warn(message: string, meta?: any): void;
33
+ error(message: string, meta?: any): void;
34
+ }
35
+
36
+ /**
37
+ * Key-value store interface for persistence
38
+ */
39
+ export interface KeyValueStore {
40
+ /**
41
+ * Get value by key
42
+ * @returns Value as string, or null if not found
43
+ */
44
+ get(key: string): Promise<string | null>;
45
+
46
+ /**
47
+ * Set value with optional TTL
48
+ * @param key - Key to set
49
+ * @param value - Value (will be JSON stringified)
50
+ * @param ttl - Time to live in seconds (optional)
51
+ */
52
+ set(key: string, value: string, ttl?: number): Promise<void>;
53
+
54
+ /**
55
+ * Delete key
56
+ */
57
+ delete(key: string): Promise<void>;
58
+
59
+ /**
60
+ * Scan keys by pattern (optional, for debugging)
61
+ * @param pattern - Glob pattern (e.g., "subscription:*")
62
+ */
63
+ scan?(pattern: string): Promise<string[]>;
64
+ }
65
+
66
+ /**
67
+ * Tool handler function
68
+ */
69
+ export type ToolHandler<TInput = any, TOutput = any> = (
70
+ input: TInput,
71
+ context: AuthContext
72
+ ) => Promise<TOutput>;
73
+
74
+ /**
75
+ * Tool definition
76
+ */
77
+ export interface ToolDefinition<TInput = any, TOutput = any> {
78
+ name: string;
79
+ description: string;
80
+ inputSchema: JSONSchema;
81
+ handler: ToolHandler<TInput, TOutput>;
82
+ }
83
+
84
+ /**
85
+ * Resource list item
86
+ */
87
+ export interface ResourceListItem {
88
+ uri: string;
89
+ name: string;
90
+ description?: string;
91
+ mimeType?: string;
92
+ }
93
+
94
+ /**
95
+ * Resource read result
96
+ */
97
+ export interface ResourceReadResult<TData = any> {
98
+ contents: TData;
99
+ metadata?: Record<string, any>;
100
+ }
101
+
102
+ /**
103
+ * Resource read options
104
+ */
105
+ export interface ResourceReadOptions {
106
+ pagination?: {
107
+ page?: number;
108
+ limit?: number;
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Subscription metadata returned by onSubscribe
114
+ */
115
+ export interface SubscriptionMetadata {
116
+ thirdPartyWebhookId: string;
117
+ metadata?: Record<string, any>;
118
+ }
119
+
120
+ /**
121
+ * Webhook change information
122
+ */
123
+ export interface WebhookChangeInfo {
124
+ resourceUri: string;
125
+ changeType: 'created' | 'updated' | 'deleted';
126
+ data?: any;
127
+ }
128
+
129
+ /**
130
+ * Resource subscription handlers
131
+ */
132
+ export interface ResourceSubscription {
133
+ /**
134
+ * Called when a client subscribes to a resource
135
+ */
136
+ onSubscribe: (
137
+ uri: string,
138
+ subscriptionId: string,
139
+ thirdPartyWebhookUrl: string,
140
+ context: AuthContext
141
+ ) => Promise<SubscriptionMetadata>;
142
+
143
+ /**
144
+ * Called when a client unsubscribes
145
+ */
146
+ onUnsubscribe: (
147
+ uri: string,
148
+ subscriptionId: string,
149
+ storedData: SubscriptionMetadata,
150
+ context: AuthContext
151
+ ) => Promise<void>;
152
+
153
+ /**
154
+ * Called when third-party webhook is received
155
+ */
156
+ onWebhook: (
157
+ subscriptionId: string,
158
+ payload: any,
159
+ headers: Record<string, string>
160
+ ) => Promise<WebhookChangeInfo | null>;
161
+ }
162
+
163
+ /**
164
+ * Resource definition
165
+ */
166
+ export interface ResourceDefinition<TData = any> {
167
+ uri: string;
168
+ name: string;
169
+ description: string;
170
+ mimeType?: string;
171
+
172
+ read: (
173
+ uri: string,
174
+ context: AuthContext,
175
+ options?: ResourceReadOptions
176
+ ) => Promise<ResourceReadResult<TData>>;
177
+
178
+ list?: (context: AuthContext) => Promise<ResourceListItem[]>;
179
+
180
+ subscription?: ResourceSubscription;
181
+ }
182
+
183
+ /**
184
+ * Prompt definition
185
+ */
186
+ export interface PromptDefinition {
187
+ name: string;
188
+ description: string;
189
+ arguments?: Array<{
190
+ name: string;
191
+ description: string;
192
+ required?: boolean;
193
+ }>;
194
+ handler: (args: Record<string, any>, context: AuthContext) => Promise<{
195
+ messages: Array<{
196
+ role: 'user' | 'assistant';
197
+ content: string;
198
+ }>;
199
+ }>;
200
+ }
201
+
202
+ /**
203
+ * Webhook configuration
204
+ */
205
+ export interface WebhookConfig {
206
+ incomingPath?: string;
207
+ incomingSecret?: string;
208
+ verifyIncomingSignature?: (payload: any, signature: string, secret: string) => boolean;
209
+
210
+ outgoing?: {
211
+ timeout?: number;
212
+ retries?: number;
213
+ retryDelay?: number;
214
+ signPayload?: (payload: any, secret: string) => string;
215
+ onBeforeCall?: (url: string, payload: any) => void;
216
+ onAfterCall?: (url: string, response: any, error?: any) => void;
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Metrics configuration
222
+ */
223
+ export interface MetricsConfig {
224
+ enabled: boolean;
225
+ registry?: any; // prom-client Registry
226
+ }
227
+
228
+ /**
229
+ * Tracing configuration
230
+ */
231
+ export interface TracingConfig {
232
+ enabled: boolean;
233
+ serviceName?: string;
234
+ }
235
+
236
+ /**
237
+ * Batch configuration
238
+ */
239
+ export interface BatchConfig {
240
+ maxBatchSize?: number;
241
+ batchTimeout?: number;
242
+ }
243
+
244
+ /**
245
+ * Cache configuration
246
+ */
247
+ export interface CacheConfig {
248
+ enabled: boolean;
249
+ ttl?: number;
250
+ keyPrefix?: string;
251
+ }
252
+
253
+ /**
254
+ * Dead letter queue configuration
255
+ */
256
+ export interface DeadLetterQueueConfig {
257
+ enabled: boolean;
258
+ store: KeyValueStore;
259
+ retention?: number;
260
+ maxRetries?: number;
261
+ }
262
+
263
+ /**
264
+ * Middleware function
265
+ */
266
+ export type Middleware = (req: Request, res: Response, next: NextFunction) => void | Promise<void>;
267
+
268
+ /**
269
+ * MCP Server configuration
270
+ */
271
+ export interface MCPServerConfig {
272
+ // Server Identity
273
+ name: string;
274
+ version: string;
275
+
276
+ // HTTP Server Configuration
277
+ port?: number;
278
+ host?: string;
279
+ basePath?: string;
280
+ publicUrl: string;
281
+
282
+ // Authentication
283
+ authenticate?: AuthenticateFunction;
284
+
285
+ // Core MCP Components
286
+ tools: ToolDefinition[];
287
+ resources: ResourceDefinition[];
288
+ prompts?: PromptDefinition[];
289
+
290
+ // Storage
291
+ store: KeyValueStore;
292
+
293
+ // Webhook Configuration
294
+ webhooks?: WebhookConfig;
295
+
296
+ // Advanced Features
297
+ batch?: BatchConfig;
298
+ cache?: CacheConfig;
299
+ metrics?: MetricsConfig;
300
+ tracing?: TracingConfig;
301
+ middleware?: Middleware[];
302
+
303
+ // Logging
304
+ logger?: Logger;
305
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
306
+ }
307
+
308
+ /**
309
+ * Stored subscription data
310
+ */
311
+ export interface StoredSubscription {
312
+ uri: string;
313
+ resourceType: string;
314
+ clientCallbackUrl: string;
315
+ clientCallbackSecret?: string;
316
+ userId: string;
317
+ thirdPartyWebhookId: string;
318
+ metadata?: any;
319
+ createdAt: number;
320
+ }
321
+
322
+ /**
323
+ * MCP Error codes
324
+ */
325
+ export enum MCPErrorCode {
326
+ AuthenticationError = -32001,
327
+ ValidationError = -32002,
328
+ ResourceNotFoundError = -32003,
329
+ ToolExecutionError = -32004,
330
+ WebhookError = -32005,
331
+ StorageError = -32006,
332
+ }
333
+
334
+ /**
335
+ * MCP Server instance
336
+ */
337
+ export interface MCPServer {
338
+ start(): Promise<void>;
339
+ stop(): Promise<void>;
340
+ getApp(): any; // Express app
341
+ }