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.
- 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 +81 -9
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +57 -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 +97 -14
- package/src/types/index.ts +74 -1
|
@@ -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.
|
|
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
|
+
}
|