-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathserver.resourceMeta.ts
More file actions
443 lines (382 loc) · 15.2 KB
/
Copy pathserver.resourceMeta.ts
File metadata and controls
443 lines (382 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
import {
ResourceTemplate,
type CompleteResourceTemplateCallback
} from '@modelcontextprotocol/sdk/server/mcp.js';
import {
type McpResource,
type McpResourceCreator,
type McpResourceMetadata,
type McpResourceMetadataMetaConfig
} from './mcpSdk';
import {
buildSearchString,
isPlainObject,
listAllCombinations,
listIncrementalCombinations,
splitUri,
stringJoin
} from './server.helpers';
import { getOptions, runWithOptions } from './options.context';
/**
* Type definition for options used in setting up resource metadata.
*
* @interface SetMetadataOptions
*
* @property name - The name of the resource.
* @property baseUri - The base URI for the resource.
* @property searchParams - List of search parameters for the resource.
* @property {McpResource[2]} config - Configuration for the resource.
* @property {McpResourceMetadataMetaConfig | undefined} metaConfig - Metadata configuration for the resource.
* @property {McpResourceMetadata['complete']} complete - Completion function for the resource metadata.
* @property {McpResourceMetadata['registerAllSearchCombinations']} registerAllSearchCombinations - Boolean indicating
* whether to register all search combinations for the resource metadata.
*/
interface SetMetadataOptions {
name: string;
baseUri: string;
searchParams: string[];
config: McpResource[2];
metaConfig: McpResourceMetadataMetaConfig | undefined;
complete: McpResourceMetadata['complete'];
registerAllSearchCombinations: McpResourceMetadata['registerAllSearchCombinations'];
}
/**
* Generate a basic Markdown table with optional content wrapping.
*
* @note Consider relocating this function to somewhere like a "resourceHelpers"
* if we end up using it in multiple places.
*
* @param columnHeaders - Column headers for the table.
* @param rows - Rows of data to include in the table.
* @param [options] - Options for table generation.
* @param [options.wrapContents] - Optional array of booleans that aligns to each column and indicates whether to wrap the content.
* @returns A Markdown table string.
*/
const generateMarkdownTable = (columnHeaders: string[], rows: (string | string[])[][], { wrapContents = [] }: { wrapContents?: boolean[] } = {}) => {
const wrapValue = (value: string | string[], index: number) => {
if (!wrapContents[index]) {
return value;
}
if (Array.isArray(value)) {
return value.map(val => `\`${val}\``).join(', ');
}
return `\`${value}\``;
};
const tableRows = rows.map(row => `| ${row.slice(0, columnHeaders.length).map((cell, index) => wrapValue(cell, index)).join(' | ')} |`);
const tableHeader = `| ${columnHeaders.join(' | ')} |`;
const tableSeparator = `| ${columnHeaders.map(() => ':---').join(' | ')} |`;
return stringJoin.newline(
tableHeader,
tableSeparator,
...tableRows
);
};
/**
* Generate a standardized metadata table for resource discovery.
*
* @param param - Object parameter
* @param param.title - Heading title for the generated Markdown.
* @param param.description - Resource description/summary for the Markdown.
* @param param.params - Resource rows for the parameters Markdown table.
* @param [param.exampleUris] - Example URIs for the resource "Available Patterns" Markdown section.
* @returns Markdown content for resource metadata.
*/
const generateMetaContent = ({ title, description, params, exampleUris = [] }: {
title: string;
description: string;
params: { name: string; values: string[]; description: string }[];
exampleUris?: { label: string; uri: string }[];
}) => {
let table = '';
let examples = '';
if (params.length) {
const tableRows = params.map(({ name, values, description }) => [name, values, description]);
table = stringJoin.newline(
'',
'## Available Parameters',
'',
generateMarkdownTable(['Parameter', 'Valid Values', 'Description'], tableRows, { wrapContents: [true, true, false] })
);
}
if (exampleUris.length) {
const exampleUriLines = exampleUris.map(example => `- **${example.label}**: \`${example.uri}\``);
examples = stringJoin.newline(
'',
'## Available Patterns',
...exampleUriLines
);
}
return stringJoin.newline(
`# ${title}`,
description,
table,
examples
);
};
/**
* Get all registered URI variations for a template.
*
* @param baseUri - The base URI string.
* @param params - The variable names.
* @param [allCombos=false] - Whether to generate all permutations.
* @returns Array of formatted URI examples.
*/
const getUriVariations = (baseUri: string, params: string[], allCombos = false): string[] => {
const combinations = allCombos ? listAllCombinations(params) : listIncrementalCombinations(params);
return combinations.map(combo => {
let str = baseUri;
if (combo.length) {
str += `?${combo.map(param => `${param}=...`).join('&')}`;
}
return str;
});
};
/**
* Configures and returns metadata options based on the provided parameters and configuration.
*
* @note The `metaHandler` must be a function (sync or async) to align with the MCP SDK.
* If a provided handler is not a function, a default fallback async handler is used,
* see type `McpResourceMetadataMetaConfig`.
*
* @note The generated `metaHandler` attempts to run any related "completion" callbacks. If they
* fail, the handler silently ignores the error and continues execution. This is by design and
* related to our concept of meta-resources providing an alternative avenue for MCP clients
* lacking completion.
*
* @param {SetMetadataOptions} settings - Settings for configuring metadata options.
* @returns An object containing the configured metadata options.
*/
const setMetadataOptions = ({ name, baseUri, searchParams, metaConfig, config, complete, registerAllSearchCombinations }: SetMetadataOptions) => {
// Set basic meta-properties from config or create them.
const metaName = metaConfig?.name || `${name}-meta`;
const metaTitle = metaConfig?.title || `${config.title} Metadata`;
const metaDescription = metaConfig?.description || `Discovery manual for ${config.title}.`;
const metaMimeType = metaConfig?.mimeType || 'text/markdown';
let metaHandler = metaConfig?.metaHandler;
if (typeof metaHandler !== 'function') {
// Generated example URIs for fallback handler
const exampleUris = getUriVariations(baseUri, searchParams, Boolean(registerAllSearchCombinations)).map(uri => {
const splitSearchParams = uri.split('?')[1];
return {
label: !splitSearchParams ? 'Base View' : `Filtered View (${splitSearchParams})`,
uri
};
});
// Fallback handler for generating metadata content
metaHandler = async (passedParams: Record<string, string> | undefined) => {
const updatedParams = isPlainObject(passedParams) ? passedParams : {};
const params = [];
if (isPlainObject(complete)) {
for (const prop in complete) {
const name = prop;
const description = `Filter by ${name}`;
let values: string[] = [];
if (complete[prop]) {
try {
values = await complete[prop]('', { arguments: { ...updatedParams } });
} catch {}
}
params.push({ name, values, description });
}
}
return generateMetaContent({
title: metaTitle,
description: metaDescription,
params,
exampleUris
});
};
}
return {
metaName,
metaTitle,
metaDescription,
metaMimeType,
metaHandler
};
};
/**
* Generate related metadata URIs and parameters.
*
* @param options - Input options
* @param options.uriOrTemplate - Original URI or a `ResourceTemplate` instance to parse.
* @param options.configUri - Passed metadata configuration URI.
* @param options.searchFields - Passed metadata "searchFields" settings associated with the resource.
* @returns Breakdown used to build meta resources and templates.
* - `isMetaTemplate` - Whether the meta resource uses a `ResourceTemplate` (query variables).
* - `originalBaseUri` - Base URI of the source resource (before `/meta`), from the original template or string.
* - `originalSearchParams` - Query parameter names from the source URI template `{?...}` segment.
* - `metaBaseUri` - Static meta path (no `{?...}`), e.g. `originalBaseUri + '/meta'` or the base of `configUri`.
* - `metaUri` - Full meta URI, either `metaBaseUri` or `metaBaseUri{?a,b,...}` when there are variables.
* - `metaSearchParams` - Names included in the meta template; driven by `searchFields`, `configUri`, or the original template.
*/
const getUriBreakdown = ({ uriOrTemplate, configUri, searchFields }: {
uriOrTemplate: string | ResourceTemplate,
configUri: McpResourceMetadataMetaConfig['uri'],
searchFields: McpResourceMetadataMetaConfig['searchFields']
}) => {
const isResourceTemplate = uriOrTemplate instanceof ResourceTemplate;
let metaUri = configUri;
let metaBaseUri: string | undefined;
const tempOriginalUri = isResourceTemplate ? uriOrTemplate.uriTemplate?.toString() : uriOrTemplate;
const { base: originalBaseUri, search: searchOriginalKeys } = splitUri(tempOriginalUri);
const { search: metaSearchKeys } = metaUri ? splitUri(metaUri) : {};
const originalSearchParams = (searchOriginalKeys?.length && searchOriginalKeys) || [];
const tempMetaSearchParams = (metaSearchKeys?.length && metaSearchKeys) || [];
// If `searchFields` is set, use it regardless of length.
const metaSearchParams = (Array.isArray(searchFields) && searchFields) || (metaUri && tempMetaSearchParams) || originalSearchParams;
const isMetaTemplate = isResourceTemplate || metaSearchParams.length > 0;
if (metaUri) {
const { base } = splitUri(metaUri);
metaBaseUri = base;
} else if (originalBaseUri) {
metaBaseUri = `${originalBaseUri}/meta`;
}
metaUri = metaBaseUri;
if (metaSearchParams?.length) {
metaUri = `${metaBaseUri}{?${metaSearchParams.join(',')}}`;
}
return {
isMetaTemplate,
originalBaseUri,
originalSearchParams,
metaBaseUri,
metaUri,
metaSearchParams
};
};
/**
* Enhances and generates meta-resources for a set of resources.
*
* - Adds a new meta-resource if a configuration is provided
* - Modifies the original resource to indicate a meta-resource is available
*
* @note Review needing to apply session context. We currently don't apply it in `resource.*.ts`
* either, but it is applied in `server.ts`.
*
* @param {McpResourceCreator[]} resources - List of resource creators to process and enhance.
* @param [options] - Optional settings.
* @returns {McpResourceCreator[]} An updated list of resource creators, including any added or modified meta-resources.
*/
const setMetaResources = (resources: McpResourceCreator[], options = getOptions()) => {
const updatedResources: McpResourceCreator[] = [];
// Check each resource for meta-config
resources.forEach(resourceCreator => {
const [name, uriOrTemplate, config, callback, metadata] = resourceCreator(options);
// No meta-config available, move to the next resource
if (!metadata?.metaConfig) {
updatedResources.push(resourceCreator);
return;
}
// Get a URI breakdown
const uriBreakdown = getUriBreakdown({
uriOrTemplate,
configUri: metadata.metaConfig.uri,
searchFields: metadata.metaConfig.searchFields
});
// If no URI breakdown assume resource is still valid
if (!uriBreakdown.metaBaseUri || !uriBreakdown.metaUri || !uriBreakdown.originalBaseUri) {
updatedResources.push(resourceCreator);
return;
}
// Create a new meta-resource or template
// We still allow complete even though the intent of `meta` resource is to provide a
// way around completion for "lesser" MCP clients since technically, those clients can still
// pass a version parameter based on the meta URI template.
let metaResourceOrTemplate: string | ResourceTemplate = uriBreakdown.metaUri;
if (uriBreakdown.isMetaTemplate) {
const updatedComplete: { [variable: string]: CompleteResourceTemplateCallback; } = {};
if (isPlainObject(metadata.complete)) {
Object.entries(metadata.complete).forEach(([key, value]) => {
if (uriBreakdown.metaSearchParams.includes(key)) {
updatedComplete[key] = value;
}
});
}
metaResourceOrTemplate = new ResourceTemplate(uriBreakdown.metaUri, {
list: undefined,
...(Object.keys(updatedComplete).length ? { complete: updatedComplete } : {})
});
}
// Set meta-properties
const { metaName, metaTitle, metaDescription, metaMimeType, metaHandler } = setMetadataOptions({
name,
baseUri: uriBreakdown.originalBaseUri,
searchParams: uriBreakdown.originalSearchParams,
metaConfig: metadata.metaConfig,
config,
complete: metadata.complete,
registerAllSearchCombinations: metadata.registerAllSearchCombinations
});
// Resolve and serialize meta handler output
const resolveMetaText = async (params: Record<string, string> = {}) => {
const resourceText = await metaHandler(params);
return isPlainObject(resourceText) || Array.isArray(resourceText)
? JSON.stringify(resourceText, null, 2)
: String(resourceText);
};
// Create a new meta-resource
const metaResource = (opts = options): McpResource => {
const metaCallback: McpResource[3] = async (passedUri, variables) =>
runWithOptions(opts, async () => {
const updatedText = await resolveMetaText(variables);
return {
contents: [
{
uri: passedUri?.toString(),
mimeType: metaMimeType,
text: updatedText
}
]
};
});
return [
metaName,
metaResourceOrTemplate,
{
title: metaTitle,
description: metaDescription,
mimeType: metaMimeType
},
metaCallback
];
};
// Add the meta-resource enhancement to the existing resource
const enhancedResource = (opts = options): McpResource => {
const metaEnhancedCallback: McpResource[3] = async (passedUri, variables) =>
runWithOptions(opts, async () => {
const result = await callback(passedUri, variables);
if (!isPlainObject(result) || !Array.isArray(result.contents)) {
return result;
}
const updatedText = await resolveMetaText(variables);
const queryString = buildSearchString(variables, { prefix: true });
const metaContentUri = queryString ? `${uriBreakdown.metaBaseUri}${queryString}` : uriBreakdown.metaBaseUri;
return {
...result,
contents: [
...result.contents,
{
uri: metaContentUri,
mimeType: metaMimeType,
text: updatedText
}
]
};
});
return [name, uriOrTemplate, config, metaEnhancedCallback, metadata];
};
// Add the resources back in
updatedResources.push(metaResource);
updatedResources.push(enhancedResource);
});
return updatedResources;
};
export {
generateMetaContent,
generateMarkdownTable,
getUriBreakdown,
getUriVariations,
setMetadataOptions,
setMetaResources
};