mcp-http-webhook 1.0.6 → 1.0.7

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.
@@ -0,0 +1,339 @@
1
+ import { createMCPServer, InMemoryStore, PromptDefinition, ResourceDefinition, CompletionRef, AuthContext } from '../src';
2
+
3
+ /**
4
+ * Example: Completion Support for Prompts and Resources
5
+ *
6
+ * This example demonstrates how to implement autocomplete/completion
7
+ * for prompt arguments and resource URI parameters in MCP.
8
+ */
9
+
10
+ // Mock data for completion suggestions
11
+ const availableLanguages = ['python', 'javascript', 'typescript', 'java', 'go', 'rust'];
12
+ const availableFrameworks = {
13
+ python: ['django', 'flask', 'fastapi'],
14
+ javascript: ['express', 'nestjs', 'koa'],
15
+ typescript: ['express', 'nestjs', 'nest'],
16
+ java: ['spring', 'quarkus', 'micronaut'],
17
+ go: ['gin', 'echo', 'fiber'],
18
+ rust: ['actix', 'rocket', 'axum'],
19
+ };
20
+
21
+ const repositories = [
22
+ { owner: 'facebook', name: 'react', description: 'A JavaScript library for building user interfaces' },
23
+ { owner: 'microsoft', name: 'typescript', description: 'TypeScript is a superset of JavaScript' },
24
+ { owner: 'nodejs', name: 'node', description: 'Node.js JavaScript runtime' },
25
+ { owner: 'denoland', name: 'deno', description: 'A modern runtime for JavaScript and TypeScript' },
26
+ ];
27
+
28
+ // Example 1: Prompt with Completion Support
29
+ const codeGeneratorPrompt: PromptDefinition = {
30
+ name: 'generate-code',
31
+ description: 'Generate code based on language and framework',
32
+ arguments: [
33
+ {
34
+ name: 'language',
35
+ description: 'Programming language',
36
+ required: true,
37
+ },
38
+ {
39
+ name: 'framework',
40
+ description: 'Framework to use',
41
+ required: false,
42
+ },
43
+ {
44
+ name: 'feature',
45
+ description: 'Feature to implement',
46
+ required: true,
47
+ },
48
+ ],
49
+
50
+ handler: async (args: Record<string, any>, context: AuthContext) => {
51
+ const { language, framework, feature } = args;
52
+
53
+ return {
54
+ messages: [
55
+ {
56
+ role: 'user' as const,
57
+ content: `Generate ${language}${framework ? ` ${framework}` : ''} code for: ${feature}`,
58
+ },
59
+ {
60
+ role: 'assistant' as const,
61
+ content: `Here's a ${language}${framework ? ` ${framework}` : ''} implementation for ${feature}...`,
62
+ },
63
+ ],
64
+ };
65
+ },
66
+
67
+ // Completion handler for prompt arguments
68
+ completion: async (ref: CompletionRef, argument: string, context: AuthContext) => {
69
+ if (ref.type !== 'ref/prompt') {
70
+ return [];
71
+ }
72
+
73
+ console.log(`Providing completions for argument: ${argument}`);
74
+
75
+ // Complete 'language' argument
76
+ if (argument === 'language') {
77
+ return availableLanguages.map(lang => ({
78
+ value: lang,
79
+ label: lang.charAt(0).toUpperCase() + lang.slice(1),
80
+ description: `Use ${lang} for code generation`,
81
+ type: 'value' as const,
82
+ }));
83
+ }
84
+
85
+ // Complete 'framework' argument (context-aware based on language)
86
+ if (argument === 'framework') {
87
+ const selectedLanguage = ref.arguments?.language;
88
+ if (selectedLanguage && selectedLanguage in availableFrameworks) {
89
+ const frameworks = availableFrameworks[selectedLanguage as keyof typeof availableFrameworks];
90
+ return frameworks.map(fw => ({
91
+ value: fw,
92
+ label: fw.charAt(0).toUpperCase() + fw.slice(1),
93
+ description: `${fw} framework for ${selectedLanguage}`,
94
+ type: 'value' as const,
95
+ }));
96
+ }
97
+ // If no language selected, show all frameworks
98
+ return Object.values(availableFrameworks)
99
+ .flat()
100
+ .filter((v, i, a) => a.indexOf(v) === i) // unique
101
+ .map(fw => ({
102
+ value: fw,
103
+ label: fw.charAt(0).toUpperCase() + fw.slice(1),
104
+ type: 'value' as const,
105
+ }));
106
+ }
107
+
108
+ // Complete 'feature' argument with common features
109
+ if (argument === 'feature') {
110
+ const commonFeatures = [
111
+ 'REST API',
112
+ 'Authentication',
113
+ 'Database CRUD',
114
+ 'File Upload',
115
+ 'Websockets',
116
+ 'Caching',
117
+ ];
118
+ return commonFeatures.map(feat => ({
119
+ value: feat,
120
+ label: feat,
121
+ description: `Implement ${feat}`,
122
+ type: 'value' as const,
123
+ }));
124
+ }
125
+
126
+ return [];
127
+ },
128
+ };
129
+
130
+ // Example 2: Resource with Completion for URI Parameters
131
+ const githubResource: ResourceDefinition = {
132
+ uri: 'github://repos/{owner}/{repo}/issues',
133
+ name: 'github-issues',
134
+ description: 'GitHub repository issues',
135
+ mimeType: 'application/json',
136
+
137
+ list: async (context: AuthContext) => {
138
+ return {
139
+ resources: repositories.map(repo => ({
140
+ uri: `github://repos/${repo.owner}/${repo.name}/issues`,
141
+ name: `${repo.owner}/${repo.name} Issues`,
142
+ description: repo.description,
143
+ })),
144
+ };
145
+ },
146
+
147
+ read: async (uri: string, context: AuthContext) => {
148
+ const match = uri.match(/github:\/\/repos\/([^/]+)\/([^/]+)\/issues/);
149
+ if (!match) {
150
+ throw new Error('Invalid GitHub URI');
151
+ }
152
+
153
+ const [, owner, repo] = match;
154
+
155
+ return {
156
+ contents: {
157
+ repository: `${owner}/${repo}`,
158
+ issues: [
159
+ { id: 1, title: 'Bug fix needed', state: 'open' },
160
+ { id: 2, title: 'Feature request', state: 'open' },
161
+ ],
162
+ },
163
+ };
164
+ },
165
+
166
+ // Completion handler for URI parameters
167
+ completion: async (ref: CompletionRef, argument: string, context: AuthContext) => {
168
+ if (ref.type !== 'ref/resource') {
169
+ return [];
170
+ }
171
+
172
+ console.log(`Providing resource completions for: ${ref.uri}, argument: ${argument}`);
173
+
174
+ // Extract current values from URI
175
+ const match = ref.uri.match(/github:\/\/repos\/([^/]*)\/([^/]*)/);
176
+ const currentOwner = match?.[1] || '';
177
+ const currentRepo = match?.[2] || '';
178
+
179
+ // Complete 'owner' parameter
180
+ if (argument === 'owner' || !currentOwner) {
181
+ const owners = [...new Set(repositories.map(r => r.owner))];
182
+ return owners.map(owner => ({
183
+ value: owner,
184
+ label: owner,
185
+ description: `GitHub user/organization: ${owner}`,
186
+ type: 'value' as const,
187
+ }));
188
+ }
189
+
190
+ // Complete 'repo' parameter (filtered by owner)
191
+ if (argument === 'repo' || !currentRepo) {
192
+ const filtered = currentOwner
193
+ ? repositories.filter(r => r.owner === currentOwner)
194
+ : repositories;
195
+
196
+ return filtered.map(repo => ({
197
+ value: repo.name,
198
+ label: repo.name,
199
+ description: repo.description,
200
+ type: 'value' as const,
201
+ }));
202
+ }
203
+
204
+ return [];
205
+ },
206
+ };
207
+
208
+ // Example 3: Global Completion Handler
209
+ // This provides completions for any prompt/resource that doesn't have its own handler
210
+ async function globalCompletionHandler(
211
+ ref: CompletionRef,
212
+ argument: string,
213
+ context: AuthContext
214
+ ) {
215
+ console.log('Global completion handler called', { ref, argument });
216
+
217
+ // Provide generic suggestions
218
+ return [
219
+ {
220
+ value: 'default',
221
+ label: 'Default Value',
222
+ description: 'Use default value',
223
+ type: 'value' as const,
224
+ },
225
+ {
226
+ value: 'custom',
227
+ label: 'Custom Value',
228
+ description: 'Enter custom value',
229
+ type: 'value' as const,
230
+ },
231
+ ];
232
+ }
233
+
234
+ // Create and start the server
235
+ async function main() {
236
+ const server = createMCPServer({
237
+ name: 'completion-example',
238
+ version: '1.0.0',
239
+ publicUrl: 'http://localhost:3000',
240
+ port: 3000,
241
+
242
+ store: new InMemoryStore(),
243
+
244
+ tools: [],
245
+
246
+ prompts: [codeGeneratorPrompt],
247
+
248
+ resources: [githubResource],
249
+
250
+ // Global completion handler (optional, used as fallback)
251
+ completions: globalCompletionHandler,
252
+ });
253
+
254
+ await server.start();
255
+ console.log('🚀 Completion example server started!');
256
+ console.log('\nCompletion features:');
257
+ console.log('\n1. Prompt Argument Completion:');
258
+ console.log(' - Try typing in "language" field → suggests: python, javascript, typescript, etc.');
259
+ console.log(' - Try typing in "framework" field → context-aware suggestions based on selected language');
260
+ console.log(' - Try typing in "feature" field → suggests common features');
261
+ console.log('\n2. Resource URI Completion:');
262
+ console.log(' - Type github://repos/ → suggests owners: facebook, microsoft, nodejs, etc.');
263
+ console.log(' - Type github://repos/facebook/ → suggests repos: react');
264
+ console.log(' - Type github://repos/microsoft/ → suggests repos: typescript');
265
+ console.log('\n3. Global Fallback:');
266
+ console.log(' - Any argument without specific handler gets default suggestions');
267
+ }
268
+
269
+ /**
270
+ * USAGE NOTES:
271
+ *
272
+ * CLIENT REQUEST FORMAT:
273
+ *
274
+ * To request completions, clients send:
275
+ * {
276
+ * "jsonrpc": "2.0",
277
+ * "method": "completion/complete",
278
+ * "params": {
279
+ * "ref": {
280
+ * "type": "ref/prompt", // or "ref/resource"
281
+ * "name": "generate-code", // prompt name
282
+ * "arguments": { // current argument values (for context)
283
+ * "language": "python"
284
+ * }
285
+ * },
286
+ * "argument": {
287
+ * "name": "framework", // which argument to complete
288
+ * "value": "fla" // partial value typed so far
289
+ * }
290
+ * }
291
+ * }
292
+ *
293
+ * COMPLETION RESPONSE:
294
+ *
295
+ * Server responds with:
296
+ * {
297
+ * "jsonrpc": "2.0",
298
+ * "result": {
299
+ * "completion": {
300
+ * "values": [
301
+ * "flask",
302
+ * "fastapi"
303
+ * ],
304
+ * "total": 2,
305
+ * "hasMore": false
306
+ * }
307
+ * }
308
+ * }
309
+ *
310
+ * IMPLEMENTATION TIPS:
311
+ *
312
+ * 1. **Context-Aware Completions**: Use ref.arguments to provide context-aware suggestions
313
+ * (e.g., frameworks filtered by selected language)
314
+ *
315
+ * 2. **Fuzzy Matching**: Implement fuzzy search for better user experience
316
+ * (e.g., "ts" matches "typescript", "nestjs")
317
+ *
318
+ * 3. **Async Data**: Completion handlers can be async, so you can fetch suggestions
319
+ * from databases, APIs, or other sources
320
+ *
321
+ * 4. **Caching**: Cache frequent completions to improve performance
322
+ *
323
+ * 5. **Hierarchical**: For resources with nested parameters (like GitHub repos),
324
+ * provide completions in order (owner first, then repos for that owner)
325
+ *
326
+ * 6. **Performance**: Keep completion handlers fast (<100ms) for good UX
327
+ *
328
+ * 7. **Fallback**: Use global completion handler for arguments without
329
+ * specific handlers
330
+ *
331
+ * 8. **Type Safety**: Use TypeScript types to ensure completion items
332
+ * match the expected format
333
+ */
334
+
335
+ if (require.main === module) {
336
+ main().catch(console.error);
337
+ }
338
+
339
+ export { codeGeneratorPrompt, githubResource, globalCompletionHandler };
@@ -0,0 +1,261 @@
1
+ import { createMCPServer, InMemoryStore, ResourceDefinition, PaginationMetadata } from '../src';
2
+
3
+ /**
4
+ * Example: Pagination Support in MCP Resources
5
+ *
6
+ * This example demonstrates how to implement pagination in both list() and read()
7
+ * methods for MCP resources. Pagination helps handle large datasets efficiently.
8
+ */
9
+
10
+ // Mock data for demonstration
11
+ const allItems = Array.from({ length: 100 }, (_, i) => ({
12
+ id: `item-${i + 1}`,
13
+ name: `Item ${i + 1}`,
14
+ description: `Description for item ${i + 1}`,
15
+ timestamp: Date.now() - i * 1000,
16
+ }));
17
+
18
+ // Helper function to paginate array data
19
+ function paginateArray<T>(
20
+ items: T[],
21
+ page: number = 1,
22
+ limit: number = 10
23
+ ): { items: T[]; pagination: PaginationMetadata } {
24
+ const offset = (page - 1) * limit;
25
+ const paginatedItems = items.slice(offset, offset + limit);
26
+
27
+ return {
28
+ items: paginatedItems,
29
+ pagination: {
30
+ page,
31
+ limit,
32
+ total: items.length,
33
+ hasMore: offset + limit < items.length,
34
+ },
35
+ };
36
+ }
37
+
38
+ // Example 1: Paginated List Resource
39
+ const paginatedListResource: ResourceDefinition = {
40
+ uri: 'items://list',
41
+ name: 'items-list',
42
+ description: 'List of items with pagination support',
43
+ mimeType: 'application/json',
44
+
45
+ // List handler with pagination
46
+ list: async (context, options) => {
47
+ const page = options?.pagination?.page || 1;
48
+ const limit = options?.pagination?.limit || 10;
49
+
50
+ console.log(`Listing items: page=${page}, limit=${limit}`);
51
+
52
+ const { items, pagination } = paginateArray(allItems, page, limit);
53
+
54
+ return {
55
+ resources: items.map(item => ({
56
+ uri: `items://list/${item.id}`,
57
+ name: item.name,
58
+ description: item.description,
59
+ })),
60
+ pagination,
61
+ };
62
+ },
63
+
64
+ // Read handler (single item, no pagination needed)
65
+ read: async (uri, context) => {
66
+ const itemId = uri.split('/').pop();
67
+ const item = allItems.find(i => i.id === itemId);
68
+
69
+ if (!item) {
70
+ throw new Error(`Item not found: ${itemId}`);
71
+ }
72
+
73
+ return {
74
+ contents: item,
75
+ };
76
+ },
77
+ };
78
+
79
+ // Example 2: Paginated Read Resource (for large content)
80
+ const paginatedReadResource: ResourceDefinition = {
81
+ uri: 'logs://stream/{streamId}',
82
+ name: 'log-stream',
83
+ description: 'Log stream with paginated read',
84
+ mimeType: 'text/plain',
85
+
86
+ list: async (context) => {
87
+ return {
88
+ resources: [
89
+ { uri: 'logs://stream/app-logs', name: 'Application Logs' },
90
+ { uri: 'logs://stream/error-logs', name: 'Error Logs' },
91
+ { uri: 'logs://stream/access-logs', name: 'Access Logs' },
92
+ ],
93
+ };
94
+ },
95
+
96
+ // Read handler with pagination for large log files
97
+ read: async (uri, context, options) => {
98
+ const streamId = uri.split('/').pop();
99
+ const page = options?.pagination?.page || 1;
100
+ const limit = options?.pagination?.limit || 50;
101
+
102
+ console.log(`Reading logs: stream=${streamId}, page=${page}, limit=${limit}`);
103
+
104
+ // Simulate fetching log lines
105
+ const allLogLines = Array.from(
106
+ { length: 500 },
107
+ (_, i) => `[${new Date(Date.now() - i * 1000).toISOString()}] Log entry ${i + 1}`
108
+ );
109
+
110
+ const { items: logLines, pagination } = paginateArray(allLogLines, page, limit);
111
+
112
+ return {
113
+ contents: logLines.join('\n'),
114
+ pagination,
115
+ };
116
+ },
117
+ };
118
+
119
+ // Example 3: Cursor-based Pagination (for real-time data)
120
+ interface CursorData {
121
+ items: any[];
122
+ nextCursor?: string;
123
+ prevCursor?: string;
124
+ }
125
+
126
+ const cursorPaginatedResource: ResourceDefinition = {
127
+ uri: 'feed://updates',
128
+ name: 'updates-feed',
129
+ description: 'Real-time updates with cursor-based pagination',
130
+ mimeType: 'application/json',
131
+
132
+ list: async (context) => {
133
+ return {
134
+ resources: [
135
+ { uri: 'feed://updates', name: 'Updates Feed' },
136
+ ],
137
+ };
138
+ },
139
+
140
+ read: async (uri, context, options) => {
141
+ const cursor = options?.pagination?.cursor;
142
+ const limit = options?.pagination?.limit || 20;
143
+
144
+ console.log(`Reading feed: cursor=${cursor}, limit=${limit}`);
145
+
146
+ // In a real implementation, you would:
147
+ // 1. Decode the cursor to get the last seen item ID/timestamp
148
+ // 2. Query items after that cursor
149
+ // 3. Generate a new cursor for the next page
150
+
151
+ // Simulate cursor-based pagination
152
+ const cursorIndex = cursor ? parseInt(cursor, 10) : 0;
153
+ const feedItems = allItems.slice(cursorIndex, cursorIndex + limit);
154
+
155
+ const hasMore = cursorIndex + limit < allItems.length;
156
+ const nextCursor = hasMore ? String(cursorIndex + limit) : undefined;
157
+ const prevCursor = cursorIndex > 0 ? String(Math.max(0, cursorIndex - limit)) : undefined;
158
+
159
+ return {
160
+ contents: feedItems,
161
+ pagination: {
162
+ page: 1, // Not used in cursor pagination
163
+ limit,
164
+ total: allItems.length,
165
+ hasMore,
166
+ nextCursor,
167
+ prevCursor,
168
+ },
169
+ };
170
+ },
171
+ };
172
+
173
+ // Create and start the server
174
+ async function main() {
175
+ const server = createMCPServer({
176
+ name: 'pagination-example',
177
+ version: '1.0.0',
178
+ publicUrl: 'http://localhost:3000',
179
+ port: 3000,
180
+
181
+ store: new InMemoryStore(),
182
+
183
+ tools: [],
184
+
185
+ resources: [
186
+ paginatedListResource,
187
+ paginatedReadResource,
188
+ cursorPaginatedResource,
189
+ ],
190
+ });
191
+
192
+ await server.start();
193
+ console.log('🚀 Pagination example server started!');
194
+ console.log('\nTry these requests:');
195
+ console.log('\n1. List items with pagination:');
196
+ console.log(' POST http://localhost:3000/mcp');
197
+ console.log(' Body: {"method": "resources/list", "_meta": {"page": 1, "limit": 5}}');
198
+ console.log('\n2. Read logs with pagination:');
199
+ console.log(' POST http://localhost:3000/mcp');
200
+ console.log(' Body: {"method": "resources/read", "params": {"uri": "logs://stream/app-logs"}, "_meta": {"page": 2, "limit": 10}}');
201
+ console.log('\n3. Feed with cursor pagination:');
202
+ console.log(' POST http://localhost:3000/mcp');
203
+ console.log(' Body: {"method": "resources/read", "params": {"uri": "feed://updates"}, "_meta": {"cursor": "20", "limit": 20}}');
204
+ }
205
+
206
+ // Usage notes:
207
+ /**
208
+ * PAGINATION REQUEST FORMAT:
209
+ *
210
+ * Clients should send pagination parameters in the _meta field:
211
+ *
212
+ * {
213
+ * "jsonrpc": "2.0",
214
+ * "id": 1,
215
+ * "method": "resources/list",
216
+ * "params": {},
217
+ * "_meta": {
218
+ * "page": 1, // Page number (1-based)
219
+ * "limit": 10, // Items per page
220
+ * "cursor": "..." // For cursor-based pagination
221
+ * }
222
+ * }
223
+ *
224
+ * PAGINATION RESPONSE FORMAT:
225
+ *
226
+ * The response will include pagination metadata in _meta:
227
+ *
228
+ * {
229
+ * "jsonrpc": "2.0",
230
+ * "id": 1,
231
+ * "result": {
232
+ * "resources": [...],
233
+ * "_meta": {
234
+ * "pagination": {
235
+ * "page": 1,
236
+ * "limit": 10,
237
+ * "total": 100,
238
+ * "hasMore": true,
239
+ * "nextCursor": "...",
240
+ * "prevCursor": "..."
241
+ * }
242
+ * }
243
+ * }
244
+ * }
245
+ *
246
+ * IMPLEMENTATION TIPS:
247
+ *
248
+ * 1. Always provide sensible defaults (e.g., page=1, limit=10)
249
+ * 2. Set maximum limits to prevent abuse (e.g., max 100 items per page)
250
+ * 3. Return total count when possible (helps clients show "X of Y")
251
+ * 4. Use cursor-based pagination for real-time/streaming data
252
+ * 5. Use offset-based pagination for stable, sorted datasets
253
+ * 6. Include hasMore flag to indicate if more data is available
254
+ * 7. Consider caching paginated results for frequently accessed pages
255
+ */
256
+
257
+ if (require.main === module) {
258
+ main().catch(console.error);
259
+ }
260
+
261
+ export { paginatedListResource, paginatedReadResource, cursorPaginatedResource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-http-webhook",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Production-ready MCP server framework with HTTP + webhook-based subscriptions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,10 +32,10 @@
32
32
  "@ngrok/ngrok": "^1.5.2",
33
33
  "@octokit/rest": "^20.0.0",
34
34
  "cors": "^2.8.5",
35
+ "googleapis": "^144.0.0",
35
36
  "nanoid": "^5.0.0",
36
37
  "ngrok": "^4.3.3",
37
- "zod": "^3.22.0",
38
- "googleapis": "^144.0.0"
38
+ "zod": "^3.22.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/cors": "^2.8.19",
@@ -47,6 +47,7 @@
47
47
  "eslint": "^8.0.0",
48
48
  "jest": "^29.5.0",
49
49
  "prettier": "^3.0.0",
50
+ "supertest": "^7.1.4",
50
51
  "ts-jest": "^29.1.0",
51
52
  "typescript": "^5.0.0"
52
53
  },
@@ -54,4 +55,4 @@
54
55
  "ioredis": "^5.3.0",
55
56
  "prom-client": "^15.0.0"
56
57
  }
57
- }
58
+ }