-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatoms.js
More file actions
343 lines (296 loc) · 13.5 KB
/
Copy pathatoms.js
File metadata and controls
343 lines (296 loc) · 13.5 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
/**
* @file atoms.js
*
* Wires the MCP server with dynamically-generated definitions
* provided by `builderBot()`.
*
* builderBot(basePath) → Promise<{
* [folderName]: [
* { name, title, description, inputSchema, outputSchema, defaultExport, file, mimeType }
* ]
* }>
*
* Registration patterns:
* - Tools: server.registerTool(name, { title, description, inputSchema }, handler)
* - inputSchema is converted to JSON Schema format with required: ['field1', 'field2'] array
* - Prompts: server.registerPrompt(name, { title, description, argsSchema }, handler)
* - Prompts should return { messages: [...] } format
* - argsSchema is extracted from inputSchema shape (Zod schema)
* - Required/optional determined by isSchemaOptional(), converted to PromptArgument[] with required: boolean
* - Resources: server.registerResource(resourceName, resourceUri, { title, description, inputSchema }, handler)
* - ResourceTemplates do NOT support inputSchema validation or required field definitions
* - Variables are extracted from URI pattern only (e.g., {userId} from 'users://{userId}/profile')
* - inputSchema in metadata is for documentation purposes only
*/
import path from 'path';
//import scribbles from 'scribbles';
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
//import { ListPromptsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { getObjectShape, getSchemaDescription } from '@modelcontextprotocol/sdk/server/zod-compat.js';
import { isSchemaOptional as sdkIsSchemaOptional } from '@modelcontextprotocol/sdk/server/zod-compat.js';
import builderBot, { segmentsAfterStart, buildResourceName, buildResourceUri } from './builderBot/index.js';
import packageJson from './package.json' with { type: 'json' };
// Workaround for SDK bug: isSchemaOptional doesn't check isOptional() method for Zod v4
// v4 has isOptional() method and _zod.def.type === 'optional', but SDK only checks typeName === 'ZodOptional'
// Since we can't patch ES module exports, we'll patch the McpServer's setPromptRequestHandlers method
// to use a fixed version of promptArgumentsFromSchema
// Fixed isSchemaOptional that checks isOptional() method for both v3 and v4
function fixedIsSchemaOptional(schema) {
// Check isOptional() method first for both v3 and v4 (v4 has this method too)
if (typeof schema.isOptional === 'function') {
return schema.isOptional();
}
// Fall back to SDK's original implementation
return sdkIsSchemaOptional(schema);
}
// Patch the server's setPromptRequestHandlers to use our fixed promptArgumentsFromSchema
// This fixes the bug where optional fields aren't detected for Zod v4
// NOTE: This patching code is moved into robotIsworking function where server is available
// The original code here referenced 'server' before it was defined, causing module load errors
//
// This patching should be done per-server instance inside robotIsworking if needed
// ------------------------------------------------------------
// REGISTER GENERATED TOOLS
// ------------------------------------------------------------
const _startFrom = './register';
/**
* Extract arguments from a Zod schema for prompts, determining required/optional based on optional fields.
* This is used for logging/debugging purposes. The actual conversion is done by the SDK's promptArgumentsFromSchema.
*
* @param {import('zod').ZodType} schema - Zod object schema
* @returns {Array<{name: string, description?: string, required: boolean}>}
*/
function extractArgumentsFromSchema(schema) {
if (!schema) return [];
// Normalize the schema to get the object shape
const shape = getObjectShape(schema);
if (!shape) return [];
return Object.entries(shape).map(([name, field]) => {
const description = getSchemaDescription(field);
const isOptional = fixedIsSchemaOptional(field);
return {
name,
description: description || undefined,
required: !isOptional
};
});
}
/**
* robotIsworking resolves to the fully configured MCP server.
* This keeps the export simple for the consumer.
*/
const robotIsworking = (startFrom=_startFrom,server=null)=>builderBot(startFrom).then(items => {
if(!server){
server = new McpServer({
name: packageJson.name,
version: packageJson.version
});
}
for (const itemName in items) {
const tidyName = tidy4(itemName);
items[itemName].forEach(
(item) => {
const { name, title, description, inputSchema, outputSchema, mimeType, defaultExport, file, relativePath, autocomplete } = item;
// HANDLE PROMPTS specially
if (tidyName === 'Prompt') {
// Extract argsSchema from inputSchema (prompts use argsSchema, not inputSchema)
// argsSchema should be the shape object from a Zod object schema
// The shape is an object like { name: z.string(), style: z.string().optional() }
// The SDK's objectFromShape will process this shape and preserve optional fields
let argsSchema = undefined;
if (inputSchema) {
// Zod objects have a 'shape' property that contains the shape object
// Note: _def.shape is a function, not an object, so we use the shape property directly
if (typeof inputSchema === 'object' && 'shape' in inputSchema) {
argsSchema = inputSchema.shape;
}
}
// Prompt handler receives (args, extra) and should return { messages: [...] }
// The defaultExport should return { messages: [...] } format per MCP spec
const handler = async (args = {}, extra = {}) => {
// Call defaultExport with the arguments
const result = await Promise.resolve(defaultExport(args));
// If result already has messages array, return it directly (expected format)
if (result && typeof result === 'object' && Array.isArray(result.messages)) {
return result;
}
// Fallback: if result doesn't have messages, wrap it
// This handles cases where the function returns a string or other format
const text = typeof result === 'string'
? result
: (result && typeof result === 'object' && result.text)
? result.text
: JSON.stringify(result);
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: text
}
}
]
};
};
// Verify argsSchema before registration - check required/optional
if (argsSchema && inputSchema) {
// Log what we're passing to the SDK
// scribbles.log(`[atoms] Prompt ${name} argsSchema keys:`, Object.keys(argsSchema));
for (const [key, field] of Object.entries(argsSchema)) {
const isOptional = field && typeof field === 'object' &&
(field.constructor.name === 'ZodOptional' ||
(typeof field.isOptional === 'function' && field.isOptional()));
//scribbles.log(` ${key}: type=${field?.constructor?.name}, isOptional=${isOptional}, required=${!isOptional}`);
}
// Also test using our helper function
const testArgs = extractArgumentsFromSchema(inputSchema);
// scribbles.log(`[atoms] Prompt ${name} extracted arguments (from inputSchema):`, testArgs);
}
// Register the prompt
server.registerPrompt(
name,
{
title,
description,
argsSchema
},
handler
);
return; // continue to next item
} // END if Prompt
// HANDLE RESOURCES specially
if (tidyName === 'Resource') {
// Compute segments after the startFrom folder so we can build the resource name & uri.
// Use relativePath if available (from recursive fileWalker), otherwise compute from file path
const segs = relativePath
? relativePath.split(path.sep).filter(Boolean)
: segmentsAfterStart(file, startFrom);
// Registration NAME (dash-joined, placeholders removed)
const registrationName = buildResourceName(segs);
// Resource URI like "users://{userId}/profile" or "config://app"
const resourceUriString = buildResourceUri(segs);
// Check if URI contains placeholders (e.g., {userId})
// Note: ResourceTemplates do NOT support inputSchema or required field definitions
// Variables are extracted from the URI pattern only
const hasParameters = /\{[^}]+\}/.test(resourceUriString);
// meta passed to registerResource
// Note: inputSchema is included for documentation purposes only, not for validation
const meta = {
title,
description,
inputSchema // For documentation only - ResourceTemplates don't validate this
};
// Register the resource on the server.
// Use ResourceTemplate if it has parameters, otherwise use plain URI string
if (hasParameters) {
// Dynamic resource with parameters - use ResourceTemplate
// Handler signature: (uri: URL, variables: Variables, extra)
// Build template config with autocomplete if present
const templateConfig = {
list: undefined // No list of available resources
};
// Add complete functions if autocomplete export exists
// Check that autocomplete is a plain object (not array) with function values
if (autocomplete &&
typeof autocomplete === 'object' &&
!Array.isArray(autocomplete) &&
Object.values(autocomplete).some(v => typeof v === 'function')) {
templateConfig.complete = autocomplete;
// scribbles.log(`[atoms] Adding autocomplete for ${registrationName}:`, autocomplete);
} /*else {
scribbles.log(`[atoms] No autocomplete found for ${registrationName}`, autocomplete);
}*/
const resourceTemplate = new ResourceTemplate(resourceUriString, templateConfig);
const handler = async (uri, variables={}, extra={}) => {
// variables contains extracted URI parameters (e.g., { userId: "123" })
const raw = await Promise.resolve(defaultExport({uri, ...variables, ...extra}));
// If already an array, assume it's already in the correct format
if (Array.isArray(raw)) {
return { contents: raw };
}
const output = raw;
const isObject = output !== null && typeof output === 'object';
const primaryMime = mimeType || (isObject ? 'application/json' : 'text/plain');
const primaryText = isObject ? JSON.stringify(output) : String(output);
return {
contents: [{
uri: uri.href,
mimeType: primaryMime,
text: primaryText
}]
};
};
server.registerResource(registrationName,
resourceTemplate,
meta,
handler);
} else {
// Static resource without parameters - use plain URI string
// Handler signature: (uri: URL, extra)
const handler = async (uri, extra={}) => {
// No parameters, pass empty object to defaultExport
const raw = await Promise.resolve(defaultExport({uri, ...extra}));
// If already an array, assume it's already in the correct format
if (Array.isArray(raw)) {
return { contents: raw };
}
const output = raw;
const isObject = output !== null && typeof output === 'object';
const primaryMime = mimeType || (isObject ? 'application/json' : 'text/plain');
const primaryText = isObject ? JSON.stringify(output) : String(output);
return {
contents: [{
uri: uri.href,
mimeType: primaryMime,
text: primaryText
}]
};
};
server.registerResource(registrationName,
resourceUriString,
meta,
handler);
}
return; // continue to next item
} // END if Resource
// Default non-resource registration:
server[`register${tidyName}`](
name,
{ title, description, inputSchema },
(...args) => {
return Promise.resolve(defaultExport(...args)).then(result => {
let output = result, more = [];
if(Array.isArray(result)){
const [a, ...b] = result;
output = a;
more = b;
}
return {
content: [
{ type: 'text', text: JSON.stringify(output) },
...more
],
structuredContent: output
}; // END return
})
} // END handler function
); // END server.register...
} // END forEach item
); // END forEach
} // END for..in items
return server;
}); // END robotIsworking
export default robotIsworking;
/**
* Convert a folder name like "tools" → "Tool".
* Removes trailing "s" and capitalises the first letter.
*
* @param {string} s
* @returns {string}
*/
function tidy4(s) {
return (s || '')
.replace(/s$/i, '')
.replace(/^./, c => c.toUpperCase())
}