mcp-http-webhook 1.0.5 → 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.
- package/COMPLETION_IMPLEMENTATION.md +280 -0
- package/PAGINATION_IMPLEMENTATION.md +221 -0
- package/README.md +194 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +159 -9
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +76 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/examples/completion-example.ts +339 -0
- package/examples/pagination-example.ts +261 -0
- package/package.json +5 -4
- package/src/__tests__/pagination.test.ts +323 -0
- package/src/server.ts +203 -15
- package/src/types/index.ts +112 -1
|
@@ -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,7 +2,8 @@ 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 {
|
|
5
|
+
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
|
6
|
+
import { MCPServerConfig, MCPServer, Logger, JSONSchema, CredentialFieldDefinition } from './types';
|
|
6
7
|
import { SubscriptionManager } from './subscriptions/SubscriptionManager';
|
|
7
8
|
import { WebhookManager } from './webhooks/WebhookManager';
|
|
8
9
|
import { MCPError, AuthenticationError, ValidationError } from './errors';
|
|
@@ -69,6 +70,101 @@ function convertToZodSchema(jsonSchema: JSONSchema): Record<string, z.ZodTypeAny
|
|
|
69
70
|
return schema;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
const VALID_CREDENTIAL_FIELD_TYPES = new Set([
|
|
74
|
+
'string',
|
|
75
|
+
'number',
|
|
76
|
+
'boolean',
|
|
77
|
+
'object',
|
|
78
|
+
'array',
|
|
79
|
+
'date',
|
|
80
|
+
'file',
|
|
81
|
+
'binary',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
function sanitizeCredentialFields(
|
|
85
|
+
fields: unknown,
|
|
86
|
+
path = 'credentials'
|
|
87
|
+
): CredentialFieldDefinition[] {
|
|
88
|
+
if (fields === undefined || fields === null) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!Array.isArray(fields)) {
|
|
93
|
+
throw new ValidationError(`MCP configuration ${path} must be an array`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return fields.map((field, index) =>
|
|
97
|
+
sanitizeCredentialField(field, `${path}[${index}]`)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sanitizeCredentialField(
|
|
102
|
+
field: unknown,
|
|
103
|
+
path: string
|
|
104
|
+
): CredentialFieldDefinition {
|
|
105
|
+
if (typeof field !== 'object' || field === null) {
|
|
106
|
+
throw new ValidationError(`MCP configuration ${path} must be an object`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const candidate = field as Record<string, any>;
|
|
110
|
+
const rawName = candidate.name;
|
|
111
|
+
const name = typeof rawName === 'string' ? rawName.trim() : '';
|
|
112
|
+
|
|
113
|
+
if (!name) {
|
|
114
|
+
throw new ValidationError(`MCP configuration ${path}.name is required`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rawType = typeof candidate.type === 'string'
|
|
118
|
+
? candidate.type.toLowerCase()
|
|
119
|
+
: 'string';
|
|
120
|
+
const type = (VALID_CREDENTIAL_FIELD_TYPES.has(rawType)
|
|
121
|
+
? rawType
|
|
122
|
+
: 'string') as CredentialFieldDefinition['type'];
|
|
123
|
+
|
|
124
|
+
const description =
|
|
125
|
+
typeof candidate.description === 'string' && candidate.description.trim().length > 0
|
|
126
|
+
? candidate.description
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
129
|
+
const sanitized: CredentialFieldDefinition = {
|
|
130
|
+
name,
|
|
131
|
+
type,
|
|
132
|
+
required: Boolean(candidate.required),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (description) {
|
|
136
|
+
sanitized.description = description;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (candidate.default !== undefined) {
|
|
140
|
+
sanitized.default = candidate.default;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(candidate.innerFields) && candidate.innerFields.length > 0) {
|
|
144
|
+
const inner = sanitizeCredentialFields(candidate.innerFields, `${path}.innerFields`);
|
|
145
|
+
if (inner.length > 0) {
|
|
146
|
+
sanitized.innerFields = inner;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(candidate.or) && candidate.or.length > 0) {
|
|
151
|
+
const alternatives = sanitizeCredentialFields(candidate.or, `${path}.or`);
|
|
152
|
+
if (alternatives.length > 0) {
|
|
153
|
+
sanitized.or = alternatives;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (candidate.config && typeof candidate.config === 'object') {
|
|
158
|
+
sanitized.config = { ...candidate.config };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (candidate.metadata && typeof candidate.metadata === 'object') {
|
|
162
|
+
sanitized.metadata = { ...candidate.metadata };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return sanitized;
|
|
166
|
+
}
|
|
167
|
+
|
|
72
168
|
/**
|
|
73
169
|
* Create MCP HTTP Webhook Server using standard MCP SDK
|
|
74
170
|
*/
|
|
@@ -80,6 +176,8 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
80
176
|
|
|
81
177
|
// Validate configuration
|
|
82
178
|
validateConfig(config);
|
|
179
|
+
// Validate credential metadata upfront to surface configuration issues early
|
|
180
|
+
sanitizeCredentialFields(config.credentials);
|
|
83
181
|
|
|
84
182
|
// Create Express app
|
|
85
183
|
const app: Express = express();
|
|
@@ -108,6 +206,7 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
108
206
|
experimental: {
|
|
109
207
|
isConnector: true,
|
|
110
208
|
},
|
|
209
|
+
"completions": {}
|
|
111
210
|
},
|
|
112
211
|
}
|
|
113
212
|
);
|
|
@@ -158,12 +257,28 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
158
257
|
if (isTemplate && resourceDef.list) {
|
|
159
258
|
// Use ResourceTemplate for templated URIs
|
|
160
259
|
const template = new ResourceTemplate(resourceDef.uri, {
|
|
161
|
-
list: async () => {
|
|
260
|
+
list: async (extra?: any) => {
|
|
162
261
|
const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
|
|
163
|
-
|
|
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;
|
|
164
279
|
|
|
165
280
|
// Map to MCP SDK Resource format with index signature
|
|
166
|
-
|
|
281
|
+
const response: any = {
|
|
167
282
|
resources: resources.map((r: any) => ({
|
|
168
283
|
uri: r.uri,
|
|
169
284
|
name: r.name,
|
|
@@ -173,6 +288,13 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
173
288
|
...r, // Spread to add index signature
|
|
174
289
|
})),
|
|
175
290
|
};
|
|
291
|
+
|
|
292
|
+
// Add pagination metadata if present
|
|
293
|
+
if (pagination) {
|
|
294
|
+
response._meta = { pagination };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return response;
|
|
176
298
|
},
|
|
177
299
|
});
|
|
178
300
|
sdkServer.registerResource(
|
|
@@ -182,9 +304,21 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
182
304
|
description: resourceDef.description,
|
|
183
305
|
mimeType: resourceDef.mimeType || 'application/json',
|
|
184
306
|
},
|
|
185
|
-
async (uri
|
|
307
|
+
async (uri, _variables, _extra) => {
|
|
186
308
|
const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
|
|
187
|
-
|
|
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);
|
|
188
322
|
|
|
189
323
|
const metadata =
|
|
190
324
|
result.metadata ||
|
|
@@ -214,6 +348,11 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
214
348
|
if (metadata) {
|
|
215
349
|
response.metadata = metadata;
|
|
216
350
|
}
|
|
351
|
+
|
|
352
|
+
// Add pagination metadata if present
|
|
353
|
+
if (result.pagination) {
|
|
354
|
+
response._meta = { pagination: result.pagination };
|
|
355
|
+
}
|
|
217
356
|
|
|
218
357
|
return response;
|
|
219
358
|
}
|
|
@@ -229,7 +368,19 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
229
368
|
},
|
|
230
369
|
async (uri: any, _extra: any) => {
|
|
231
370
|
const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
|
|
232
|
-
|
|
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);
|
|
233
384
|
|
|
234
385
|
const metadata =
|
|
235
386
|
result.metadata ||
|
|
@@ -259,6 +410,11 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
259
410
|
if (metadata) {
|
|
260
411
|
response.metadata = metadata;
|
|
261
412
|
}
|
|
413
|
+
|
|
414
|
+
// Add pagination metadata if present
|
|
415
|
+
if (result.pagination) {
|
|
416
|
+
response._meta = { pagination: result.pagination };
|
|
417
|
+
}
|
|
262
418
|
|
|
263
419
|
return response;
|
|
264
420
|
}
|
|
@@ -269,19 +425,43 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
269
425
|
// Register prompts with MCP SDK
|
|
270
426
|
if (config.prompts) {
|
|
271
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
|
+
|
|
272
459
|
sdkServer.registerPrompt(
|
|
273
460
|
promptDef.name,
|
|
274
461
|
{
|
|
275
462
|
title: promptDef.name,
|
|
276
463
|
description: promptDef.description,
|
|
277
|
-
argsSchema
|
|
278
|
-
promptDef.arguments?.reduce(
|
|
279
|
-
(schema, arg) => {
|
|
280
|
-
schema[arg.name] = z.string();
|
|
281
|
-
return schema;
|
|
282
|
-
},
|
|
283
|
-
{} as Record<string, any>
|
|
284
|
-
) || {},
|
|
464
|
+
argsSchema,
|
|
285
465
|
},
|
|
286
466
|
async (args: any) => {
|
|
287
467
|
const context = (sdkServer as any)._currentContext || { userId: 'anonymous' };
|
|
@@ -481,6 +661,14 @@ export function createMCPServer(config: MCPServerConfig): MCPServer {
|
|
|
481
661
|
})
|
|
482
662
|
);
|
|
483
663
|
|
|
664
|
+
app.get(`${basePath}/.well-known/credentials`, asyncHandler(async (req, res) => {
|
|
665
|
+
const credentials = sanitizeCredentialFields(config.credentials);
|
|
666
|
+
res.json({
|
|
667
|
+
version: config.version,
|
|
668
|
+
credentials,
|
|
669
|
+
});
|
|
670
|
+
}));
|
|
671
|
+
|
|
484
672
|
// Webhook endpoints
|
|
485
673
|
const webhookPath = config.webhooks?.incomingPath || '/webhooks/incoming';
|
|
486
674
|
|