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,323 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
2
+ import request from 'supertest';
3
+ import { createMCPServer, InMemoryStore, ResourceDefinition, AuthContext, ResourceReadOptions } from '../index';
4
+
5
+ describe('Pagination Support', () => {
6
+ let server: any;
7
+ let app: any;
8
+
9
+ // Mock data
10
+ const mockItems = Array.from({ length: 50 }, (_, i) => ({
11
+ id: `item-${i + 1}`,
12
+ name: `Item ${i + 1}`,
13
+ data: `Data for item ${i + 1}`,
14
+ }));
15
+
16
+ beforeAll(async () => {
17
+ const paginatedResource: ResourceDefinition = {
18
+ uri: 'test://items',
19
+ name: 'test-items',
20
+ description: 'Test resource with pagination',
21
+ mimeType: 'application/json',
22
+
23
+ list: async (context: AuthContext, options?: ResourceReadOptions) => {
24
+ const page = options?.pagination?.page || 1;
25
+ const limit = options?.pagination?.limit || 10;
26
+ const offset = (page - 1) * limit;
27
+
28
+ const paginatedItems = mockItems.slice(offset, offset + limit);
29
+
30
+ return {
31
+ resources: paginatedItems.map(item => ({
32
+ uri: `test://items/${item.id}`,
33
+ name: item.name,
34
+ })),
35
+ pagination: {
36
+ page,
37
+ limit,
38
+ total: mockItems.length,
39
+ hasMore: offset + limit < mockItems.length,
40
+ },
41
+ };
42
+ },
43
+
44
+ read: async (uri: string, context: AuthContext, options?: ResourceReadOptions) => {
45
+ const itemId = uri.split('/').pop();
46
+ const item = mockItems.find(i => i.id === itemId);
47
+
48
+ if (!item) {
49
+ throw new Error('Item not found');
50
+ }
51
+
52
+ // Support pagination for large content
53
+ const page = options?.pagination?.page || 1;
54
+ const limit = options?.pagination?.limit || 100;
55
+
56
+ return {
57
+ contents: item,
58
+ pagination: options?.pagination
59
+ ? {
60
+ page,
61
+ limit,
62
+ total: 1,
63
+ hasMore: false,
64
+ }
65
+ : undefined,
66
+ };
67
+ },
68
+ };
69
+
70
+ const cursorResource: ResourceDefinition = {
71
+ uri: 'test://cursor',
72
+ name: 'test-cursor',
73
+ description: 'Test resource with cursor pagination',
74
+ mimeType: 'application/json',
75
+
76
+ list: async (_context: AuthContext) => {
77
+ return {
78
+ resources: [{ uri: 'test://cursor', name: 'Cursor Resource' }],
79
+ };
80
+ },
81
+
82
+ read: async (uri: string, context: AuthContext, options?: ResourceReadOptions) => {
83
+ const cursor = options?.pagination?.cursor;
84
+ const limit = options?.pagination?.limit || 10;
85
+ const cursorIndex = cursor ? parseInt(cursor, 10) : 0;
86
+
87
+ const paginatedItems = mockItems.slice(cursorIndex, cursorIndex + limit);
88
+ const hasMore = cursorIndex + limit < mockItems.length;
89
+
90
+ return {
91
+ contents: paginatedItems,
92
+ pagination: {
93
+ page: 1,
94
+ limit,
95
+ total: mockItems.length,
96
+ hasMore,
97
+ nextCursor: hasMore ? String(cursorIndex + limit) : undefined,
98
+ prevCursor: cursorIndex > 0 ? String(Math.max(0, cursorIndex - limit)) : undefined,
99
+ },
100
+ };
101
+ },
102
+ };
103
+
104
+ server = createMCPServer({
105
+ name: 'pagination-test',
106
+ version: '1.0.0',
107
+ publicUrl: 'http://localhost:3001',
108
+ port: 3001,
109
+ store: new InMemoryStore(),
110
+ tools: [],
111
+ resources: [paginatedResource, cursorResource],
112
+ });
113
+
114
+ await server.start();
115
+ app = server.getApp();
116
+ });
117
+
118
+ afterAll(async () => {
119
+ await server.stop();
120
+ });
121
+
122
+ describe('List with Pagination', () => {
123
+ it('should return first page by default', async () => {
124
+ const response = await request(app)
125
+ .post('/mcp')
126
+ .send({
127
+ jsonrpc: '2.0',
128
+ id: 1,
129
+ method: 'resources/list',
130
+ params: {},
131
+ });
132
+
133
+ expect(response.status).toBe(200);
134
+ expect(response.body.result).toBeDefined();
135
+ expect(response.body.result.resources).toHaveLength(10); // Default limit
136
+ expect(response.body.result._meta?.pagination).toMatchObject({
137
+ page: 1,
138
+ limit: 10,
139
+ total: 50,
140
+ hasMore: true,
141
+ });
142
+ });
143
+
144
+ it('should return specific page with custom limit', async () => {
145
+ const response = await request(app)
146
+ .post('/mcp')
147
+ .send({
148
+ jsonrpc: '2.0',
149
+ id: 1,
150
+ method: 'resources/list',
151
+ params: {},
152
+ _meta: {
153
+ page: 2,
154
+ limit: 5,
155
+ },
156
+ });
157
+
158
+ expect(response.status).toBe(200);
159
+ expect(response.body.result.resources).toHaveLength(5);
160
+ expect(response.body.result._meta?.pagination).toMatchObject({
161
+ page: 2,
162
+ limit: 5,
163
+ total: 50,
164
+ hasMore: true,
165
+ });
166
+ });
167
+
168
+ it('should indicate no more pages on last page', async () => {
169
+ const response = await request(app)
170
+ .post('/mcp')
171
+ .send({
172
+ jsonrpc: '2.0',
173
+ id: 1,
174
+ method: 'resources/list',
175
+ params: {},
176
+ _meta: {
177
+ page: 5,
178
+ limit: 10,
179
+ },
180
+ });
181
+
182
+ expect(response.status).toBe(200);
183
+ expect(response.body.result.resources).toHaveLength(10);
184
+ expect(response.body.result._meta?.pagination.hasMore).toBe(false);
185
+ });
186
+ });
187
+
188
+ describe('Read with Pagination', () => {
189
+ it('should read without pagination by default', async () => {
190
+ const response = await request(app)
191
+ .post('/mcp')
192
+ .send({
193
+ jsonrpc: '2.0',
194
+ id: 1,
195
+ method: 'resources/read',
196
+ params: {
197
+ uri: 'test://items/item-1',
198
+ },
199
+ });
200
+
201
+ expect(response.status).toBe(200);
202
+ expect(response.body.result.contents).toBeDefined();
203
+ expect(response.body.result._meta?.pagination).toBeUndefined();
204
+ });
205
+
206
+ it('should support pagination when requested', async () => {
207
+ const response = await request(app)
208
+ .post('/mcp')
209
+ .send({
210
+ jsonrpc: '2.0',
211
+ id: 1,
212
+ method: 'resources/read',
213
+ params: {
214
+ uri: 'test://items/item-1',
215
+ },
216
+ _meta: {
217
+ page: 1,
218
+ limit: 50,
219
+ },
220
+ });
221
+
222
+ expect(response.status).toBe(200);
223
+ expect(response.body.result.contents).toBeDefined();
224
+ expect(response.body.result._meta?.pagination).toMatchObject({
225
+ page: 1,
226
+ limit: 50,
227
+ total: 1,
228
+ hasMore: false,
229
+ });
230
+ });
231
+ });
232
+
233
+ describe('Cursor-based Pagination', () => {
234
+ it('should support cursor pagination', async () => {
235
+ const response = await request(app)
236
+ .post('/mcp')
237
+ .send({
238
+ jsonrpc: '2.0',
239
+ id: 1,
240
+ method: 'resources/read',
241
+ params: {
242
+ uri: 'test://cursor',
243
+ },
244
+ _meta: {
245
+ limit: 10,
246
+ },
247
+ });
248
+
249
+ expect(response.status).toBe(200);
250
+ expect(response.body.result.contents).toHaveLength(10);
251
+ expect(response.body.result._meta?.pagination).toMatchObject({
252
+ page: 1,
253
+ limit: 10,
254
+ total: 50,
255
+ hasMore: true,
256
+ });
257
+ expect(response.body.result._meta?.pagination.nextCursor).toBe('10');
258
+ expect(response.body.result._meta?.pagination.prevCursor).toBeUndefined();
259
+ });
260
+
261
+ it('should navigate using cursor', async () => {
262
+ const response = await request(app)
263
+ .post('/mcp')
264
+ .send({
265
+ jsonrpc: '2.0',
266
+ id: 1,
267
+ method: 'resources/read',
268
+ params: {
269
+ uri: 'test://cursor',
270
+ },
271
+ _meta: {
272
+ cursor: '20',
273
+ limit: 10,
274
+ },
275
+ });
276
+
277
+ expect(response.status).toBe(200);
278
+ expect(response.body.result.contents).toHaveLength(10);
279
+ expect(response.body.result._meta?.pagination.nextCursor).toBe('30');
280
+ expect(response.body.result._meta?.pagination.prevCursor).toBe('10');
281
+ });
282
+
283
+ it('should handle last page with cursor', async () => {
284
+ const response = await request(app)
285
+ .post('/mcp')
286
+ .send({
287
+ jsonrpc: '2.0',
288
+ id: 1,
289
+ method: 'resources/read',
290
+ params: {
291
+ uri: 'test://cursor',
292
+ },
293
+ _meta: {
294
+ cursor: '45',
295
+ limit: 10,
296
+ },
297
+ });
298
+
299
+ expect(response.status).toBe(200);
300
+ expect(response.body.result.contents).toHaveLength(5);
301
+ expect(response.body.result._meta?.pagination.hasMore).toBe(false);
302
+ expect(response.body.result._meta?.pagination.nextCursor).toBeUndefined();
303
+ expect(response.body.result._meta?.pagination.prevCursor).toBe('35');
304
+ });
305
+ });
306
+
307
+ describe('Backward Compatibility', () => {
308
+ it('should handle resources without pagination support', async () => {
309
+ // This tests that old resources still work
310
+ const response = await request(app)
311
+ .post('/mcp')
312
+ .send({
313
+ jsonrpc: '2.0',
314
+ id: 1,
315
+ method: 'resources/list',
316
+ params: {},
317
+ });
318
+
319
+ expect(response.status).toBe(200);
320
+ expect(response.body.result.resources).toBeDefined();
321
+ });
322
+ });
323
+ });
package/src/server.ts CHANGED
@@ -2,6 +2,7 @@ import express, { Express, Request, Response, NextFunction } from 'express';
2
2
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
4
  import { z } from 'zod';
5
+ import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
5
6
  import { MCPServerConfig, MCPServer, Logger, JSONSchema, CredentialFieldDefinition } from './types';
6
7
  import { SubscriptionManager } from './subscriptions/SubscriptionManager';
7
8
  import { WebhookManager } from './webhooks/WebhookManager';
@@ -205,6 +206,7 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
205
206
  experimental: {
206
207
  isConnector: true,
207
208
  },
209
+ "completions": {}
208
210
  },
209
211
  }
210
212
  );
@@ -255,12 +257,28 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
255
257
  if (isTemplate && resourceDef.list) {
256
258
  // Use ResourceTemplate for templated URIs
257
259
  const template = new ResourceTemplate(resourceDef.uri, {
258
- list: async () => {
260
+ list: async (extra?: any) => {
259
261
  const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
260
- const resources = await resourceDef.list!(context);
262
+
263
+ // Extract pagination options from extra metadata
264
+ const options: any = {};
265
+ const meta = extra?._meta as any;
266
+ if (meta?.page || meta?.limit || meta?.cursor) {
267
+ options.pagination = {
268
+ page: meta.page ? parseInt(String(meta.page), 10) : undefined,
269
+ limit: meta.limit ? parseInt(String(meta.limit), 10) : undefined,
270
+ cursor: meta.cursor,
271
+ };
272
+ }
273
+
274
+ const result = await resourceDef.list!(context, options);
275
+
276
+ // Handle both array and paginated responses
277
+ const resources = Array.isArray(result) ? result : result.resources;
278
+ const pagination = Array.isArray(result) ? undefined : result.pagination;
261
279
 
262
280
  // Map to MCP SDK Resource format with index signature
263
- return {
281
+ const response: any = {
264
282
  resources: resources.map((r: any) => ({
265
283
  uri: r.uri,
266
284
  name: r.name,
@@ -270,6 +288,13 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
270
288
  ...r, // Spread to add index signature
271
289
  })),
272
290
  };
291
+
292
+ // Add pagination metadata if present
293
+ if (pagination) {
294
+ response._meta = { pagination };
295
+ }
296
+
297
+ return response;
273
298
  },
274
299
  });
275
300
  sdkServer.registerResource(
@@ -279,9 +304,21 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
279
304
  description: resourceDef.description,
280
305
  mimeType: resourceDef.mimeType || 'application/json',
281
306
  },
282
- async (uri: any, _variables: any, _extra: any) => {
307
+ async (uri, _variables, _extra) => {
283
308
  const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
284
- const result = await resourceDef.read(uri.href, { ...context, ...(_variables || {}) });
309
+
310
+ // Extract pagination options from _extra metadata
311
+ const options: any = {};
312
+ const meta = _extra?._meta as any;
313
+ if (meta?.page || meta?.limit || meta?.cursor) {
314
+ options.pagination = {
315
+ page: meta.page ? parseInt(String(meta.page), 10) : undefined,
316
+ limit: meta.limit ? parseInt(String(meta.limit), 10) : undefined,
317
+ cursor: meta.cursor,
318
+ };
319
+ }
320
+
321
+ const result = await resourceDef.read(uri.href, { ...context, ...(_variables || {}) }, options);
285
322
 
286
323
  const metadata =
287
324
  result.metadata ||
@@ -311,6 +348,11 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
311
348
  if (metadata) {
312
349
  response.metadata = metadata;
313
350
  }
351
+
352
+ // Add pagination metadata if present
353
+ if (result.pagination) {
354
+ response._meta = { pagination: result.pagination };
355
+ }
314
356
 
315
357
  return response;
316
358
  }
@@ -326,7 +368,19 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
326
368
  },
327
369
  async (uri: any, _extra: any) => {
328
370
  const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
329
- const result = await resourceDef.read(uri.href, context);
371
+
372
+ // Extract pagination options from _extra metadata
373
+ const options: any = {};
374
+ const meta = _extra?._meta as any;
375
+ if (meta?.page || meta?.limit || meta?.cursor) {
376
+ options.pagination = {
377
+ page: meta.page ? parseInt(String(meta.page), 10) : undefined,
378
+ limit: meta.limit ? parseInt(String(meta.limit), 10) : undefined,
379
+ cursor: meta.cursor,
380
+ };
381
+ }
382
+
383
+ const result = await resourceDef.read(uri.href, context, options);
330
384
 
331
385
  const metadata =
332
386
  result.metadata ||
@@ -356,6 +410,11 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
356
410
  if (metadata) {
357
411
  response.metadata = metadata;
358
412
  }
413
+
414
+ // Add pagination metadata if present
415
+ if (result.pagination) {
416
+ response._meta = { pagination: result.pagination };
417
+ }
359
418
 
360
419
  return response;
361
420
  }
@@ -366,19 +425,43 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
366
425
  // Register prompts with MCP SDK
367
426
  if (config.prompts) {
368
427
  config.prompts.forEach((promptDef) => {
428
+ const argsSchema: Record<string, any> = {};
429
+
430
+ // Build args schema with completion support
431
+ if (promptDef.arguments) {
432
+ promptDef.arguments.forEach((arg) => {
433
+ let argSchema = z.string();
434
+
435
+ // Add completion support if handler is provided
436
+ if (promptDef.completion) {
437
+ argSchema = completable(z.string(), async (value, context) => {
438
+ const ctx = (sdkServer as any)._currentContext || { userId: 'anonymous' };
439
+ const ref = {
440
+ type: 'ref/prompt' as const,
441
+ name: promptDef.name,
442
+ arguments: context?.arguments || {},
443
+ };
444
+
445
+ try {
446
+ const completions = await promptDef.completion!(ref, arg.name, ctx);
447
+ return completions.map(c => c.value);
448
+ } catch (error) {
449
+ logger.error('Completion error', { error });
450
+ return [];
451
+ }
452
+ }) as any;
453
+ }
454
+
455
+ argsSchema[arg.name] = argSchema;
456
+ });
457
+ }
458
+
369
459
  sdkServer.registerPrompt(
370
460
  promptDef.name,
371
461
  {
372
462
  title: promptDef.name,
373
463
  description: promptDef.description,
374
- argsSchema:
375
- promptDef.arguments?.reduce(
376
- (schema, arg) => {
377
- schema[arg.name] = z.string();
378
- return schema;
379
- },
380
- {} as Record<string, any>
381
- ) || {},
464
+ argsSchema,
382
465
  },
383
466
  async (args: any) => {
384
467
  const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
@@ -197,9 +197,33 @@ export interface ResourceListItem {
197
197
  /**
198
198
  * Resource read result
199
199
  */
200
+ /**
201
+ * Pagination metadata for responses
202
+ */
203
+ export interface PaginationMetadata {
204
+ page: number;
205
+ limit: number;
206
+ total?: number;
207
+ hasMore?: boolean;
208
+ nextCursor?: string;
209
+ prevCursor?: string;
210
+ }
211
+
212
+ /**
213
+ * Resource read result with optional pagination
214
+ */
200
215
  export interface ResourceReadResult<TData = any> {
201
216
  contents: TData;
202
217
  metadata?: ResourceMetadata;
218
+ pagination?: PaginationMetadata;
219
+ }
220
+
221
+ /**
222
+ * Resource list result with optional pagination
223
+ */
224
+ export interface ResourceListResult {
225
+ resources: ResourceListItem[];
226
+ pagination?: PaginationMetadata;
203
227
  }
204
228
 
205
229
  /**
@@ -209,6 +233,7 @@ export interface ResourceReadOptions {
209
233
  pagination?: {
210
234
  page?: number;
211
235
  limit?: number;
236
+ cursor?: string;
212
237
  };
213
238
  }
214
239
 
@@ -280,11 +305,52 @@ export interface ResourceDefinition<TData = any> {
280
305
  options?: ResourceReadOptions
281
306
  ) => Promise<ResourceReadResult<TData>>;
282
307
 
283
- list?: (context: AuthContext) => Promise<ResourceListItem[]>;
308
+ list?: (
309
+ context: AuthContext,
310
+ options?: ResourceReadOptions
311
+ ) => Promise<ResourceListResult | ResourceListItem[]>;
284
312
 
285
313
  subscription?: ResourceSubscription;
314
+
315
+ /**
316
+ * Optional completion handler for resource URI parameters
317
+ */
318
+ completion?: CompletionHandler;
286
319
  }
287
320
 
321
+ /**
322
+ * Completion item returned by completion handlers
323
+ */
324
+ export interface CompletionItem {
325
+ value: string;
326
+ label?: string;
327
+ description?: string;
328
+ type?: 'value' | 'function' | 'variable' | 'constant' | 'other';
329
+ }
330
+
331
+ /**
332
+ * Completion reference - identifies what to complete
333
+ */
334
+ export type CompletionRef =
335
+ | {
336
+ type: 'ref/prompt';
337
+ name: string;
338
+ arguments?: Record<string, string>;
339
+ }
340
+ | {
341
+ type: 'ref/resource';
342
+ uri: string;
343
+ };
344
+
345
+ /**
346
+ * Completion handler function
347
+ */
348
+ export type CompletionHandler = (
349
+ ref: CompletionRef,
350
+ argument: string,
351
+ context: AuthContext
352
+ ) => Promise<CompletionItem[]>;
353
+
288
354
  /**
289
355
  * Prompt definition
290
356
  */
@@ -302,6 +368,10 @@ export interface PromptDefinition {
302
368
  content: string;
303
369
  }>;
304
370
  }>;
371
+ /**
372
+ * Optional completion handler for prompt arguments
373
+ */
374
+ completion?: CompletionHandler;
305
375
  }
306
376
 
307
377
  /**
@@ -391,6 +461,9 @@ export interface MCPServerConfig {
391
461
  tools: ToolDefinition[];
392
462
  resources: ResourceDefinition[];
393
463
  prompts?: PromptDefinition[];
464
+
465
+ // Completions
466
+ completions?: CompletionHandler;
394
467
 
395
468
  // Storage
396
469
  store: KeyValueStore;