mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Define tools that answer questions from the app's data once, independent of the surface that exposes them. A tool declares its arguments and the services it reads; applications compose tools with their own services and optional call interceptors. Queries reuse the layers' portable source factories, so they run wherever an application routes /api requests. The first queries are get_earthquakes, get_active_fires and get_recent_launches, with a shared area argument (place name, bounding box, or point and radius) and capped lists. gods-eye-view/tools/mcp adapts a catalog to the Model Context Protocol with a small JSON-RPC handler and a stateless HTTP transport, adding no dependencies. npm run mcp serves Core's tools over stdio to local clients such as Claude Code, reading from a running app. Both exports are portable and checked by check:boundaries; the app does not import them.
126 lines
4.1 KiB
JavaScript
126 lines
4.1 KiB
JavaScript
/**
|
|
* Validates tool arguments against the JSON Schema subset tool definitions use:
|
|
* object, array, string, number, integer and boolean types; required,
|
|
* properties, additionalProperties, items, enum, minimum, maximum, minLength,
|
|
* maxLength, minItems and maxItems. Unsupported keywords are rejected when a
|
|
* tool is defined, so a schema can never promise checks that are not made.
|
|
*/
|
|
|
|
const KEYWORDS = new Set([
|
|
'type',
|
|
'description',
|
|
'title',
|
|
'properties',
|
|
'required',
|
|
'additionalProperties',
|
|
'items',
|
|
'enum',
|
|
'minimum',
|
|
'maximum',
|
|
'minLength',
|
|
'maxLength',
|
|
'minItems',
|
|
'maxItems',
|
|
'default',
|
|
]);
|
|
const TYPES = new Set([
|
|
'object',
|
|
'array',
|
|
'string',
|
|
'number',
|
|
'integer',
|
|
'boolean',
|
|
]);
|
|
|
|
/** Throw when a schema uses a keyword or type this validator does not check. */
|
|
export function assertSupportedSchema(schema, path = 'schema') {
|
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
throw new TypeError(`${path} must be an object`);
|
|
for (const key of Object.keys(schema)) {
|
|
if (!KEYWORDS.has(key))
|
|
throw new TypeError(`${path} uses unsupported keyword ${key}`);
|
|
}
|
|
if (!TYPES.has(schema.type))
|
|
throw new TypeError(`${path} has unsupported type ${schema.type}`);
|
|
for (const [name, child] of Object.entries(schema.properties || {}))
|
|
assertSupportedSchema(child, `${path}.${name}`);
|
|
if (schema.items) assertSupportedSchema(schema.items, `${path}[]`);
|
|
}
|
|
|
|
/**
|
|
* Return a list of problems with a value, empty when it conforms. Messages
|
|
* name the argument path so a model can correct its call.
|
|
*/
|
|
export function validateValue(schema, value, path = 'arguments') {
|
|
const problems = [];
|
|
const fail = (message) => problems.push(`${path} ${message}`);
|
|
switch (schema.type) {
|
|
case 'object': {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
fail('must be an object');
|
|
break;
|
|
}
|
|
for (const name of schema.required || []) {
|
|
if (value[name] === undefined) fail(`is missing ${name}`);
|
|
}
|
|
const properties = schema.properties || {};
|
|
for (const [name, child] of Object.entries(value)) {
|
|
if (properties[name])
|
|
problems.push(
|
|
...validateValue(properties[name], child, `${path}.${name}`),
|
|
);
|
|
else if (schema.additionalProperties === false)
|
|
fail(`has unknown property ${name}`);
|
|
}
|
|
break;
|
|
}
|
|
case 'array':
|
|
if (!Array.isArray(value)) {
|
|
fail('must be an array');
|
|
break;
|
|
}
|
|
if (schema.minItems != null && value.length < schema.minItems)
|
|
fail(`must have at least ${schema.minItems} items`);
|
|
if (schema.maxItems != null && value.length > schema.maxItems)
|
|
fail(`must have at most ${schema.maxItems} items`);
|
|
if (schema.items)
|
|
value.forEach((item, index) =>
|
|
problems.push(
|
|
...validateValue(schema.items, item, `${path}[${index}]`),
|
|
),
|
|
);
|
|
break;
|
|
case 'string':
|
|
if (typeof value !== 'string') {
|
|
fail('must be a string');
|
|
break;
|
|
}
|
|
if (schema.minLength != null && value.length < schema.minLength)
|
|
fail(`must be at least ${schema.minLength} characters`);
|
|
if (schema.maxLength != null && value.length > schema.maxLength)
|
|
fail(`must be at most ${schema.maxLength} characters`);
|
|
break;
|
|
case 'number':
|
|
case 'integer':
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
fail('must be a number');
|
|
break;
|
|
}
|
|
if (schema.type === 'integer' && !Number.isInteger(value))
|
|
fail('must be an integer');
|
|
if (schema.minimum != null && value < schema.minimum)
|
|
fail(`must be at least ${schema.minimum}`);
|
|
if (schema.maximum != null && value > schema.maximum)
|
|
fail(`must be at most ${schema.maximum}`);
|
|
break;
|
|
case 'boolean':
|
|
if (typeof value !== 'boolean') fail('must be true or false');
|
|
break;
|
|
}
|
|
if (schema.enum && !schema.enum.includes(value))
|
|
fail(
|
|
`must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(', ')}`,
|
|
);
|
|
return problems;
|
|
}
|