mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-09-30 22:53:17 +00:00
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
|
|
/**
|
||
|
|
* OpenAPI 3.1 document validity (build-plan §5).
|
||
|
|
*
|
||
|
|
* Asserts the structural invariants of `buildOpenApiDocument`: 3.1 version,
|
||
|
|
* one `POST /tools/{name}` per tool carrying `x-required-scope`, and a
|
||
|
|
* `bearerAuth` security scheme present (design.md §2.5 — every tool path
|
||
|
|
* requires a token).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
import { ToolRegistry, defineTool } from './registry.js';
|
||
|
|
import { buildOpenApiDocument } from './openapi.js';
|
||
|
|
import type { Scope } from './types.js';
|
||
|
|
|
||
|
|
function tool(name: string, scope: Scope, tier: 'ops' | 'finance') {
|
||
|
|
return defineTool({
|
||
|
|
name,
|
||
|
|
description: `desc ${name}`,
|
||
|
|
tier,
|
||
|
|
requiredScope: scope,
|
||
|
|
inputSchema: {
|
||
|
|
type: 'object',
|
||
|
|
properties: { id: { type: 'string' } },
|
||
|
|
required: ['id'],
|
||
|
|
},
|
||
|
|
handler: async () => ({}),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function doc() {
|
||
|
|
const registry = new ToolRegistry();
|
||
|
|
registry.register(tool('lookup_thing', 'ops:read', 'ops'));
|
||
|
|
registry.register(tool('lookup_payment', 'finance:read', 'finance'));
|
||
|
|
return buildOpenApiDocument(registry, {
|
||
|
|
info: { title: 'Test', version: '1.0.0' },
|
||
|
|
servers: [{ url: 'http://localhost:8081' }],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('buildOpenApiDocument', () => {
|
||
|
|
it('declares OpenAPI 3.1.0', () => {
|
||
|
|
expect(doc().openapi).toBe('3.1.0');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('carries info and servers', () => {
|
||
|
|
const d = doc();
|
||
|
|
expect(d.info.title).toBe('Test');
|
||
|
|
expect(d.servers[0]!.url).toBe('http://localhost:8081');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('emits exactly one POST /tools/{name} per tool', () => {
|
||
|
|
const d = doc();
|
||
|
|
expect(Object.keys(d.paths).sort()).toEqual(['/tools/lookup_payment', '/tools/lookup_thing']);
|
||
|
|
for (const path of Object.values(d.paths)) {
|
||
|
|
expect(path.post).toBeDefined();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('annotates each operation with x-required-scope and a JSON request body', () => {
|
||
|
|
const op = doc().paths['/tools/lookup_payment']!.post;
|
||
|
|
expect(op['x-required-scope']).toBe('finance:read');
|
||
|
|
expect(op.requestBody.required).toBe(true);
|
||
|
|
expect(op.requestBody.content['application/json'].schema).toBeDefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('defines the bearerAuth security scheme and applies it document-wide', () => {
|
||
|
|
const d = doc();
|
||
|
|
expect(d.components.securitySchemes.bearerAuth.scheme).toBe('bearer');
|
||
|
|
expect(d.security).toEqual([{ bearerAuth: [] }]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('every operation requires the bearer token (no unauthenticated tool path)', () => {
|
||
|
|
for (const path of Object.values(doc().paths)) {
|
||
|
|
expect(path.post.security).toContainEqual({ bearerAuth: [] });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('produces a scope-filtered document when given a filtered registry', () => {
|
||
|
|
const filtered = new ToolRegistry();
|
||
|
|
filtered.register(tool('lookup_thing', 'ops:read', 'ops'));
|
||
|
|
const d = buildOpenApiDocument(filtered, {
|
||
|
|
info: { title: 'Ops', version: '1.0.0' },
|
||
|
|
servers: [{ url: 'http://x' }],
|
||
|
|
});
|
||
|
|
expect(Object.keys(d.paths)).toEqual(['/tools/lookup_thing']);
|
||
|
|
});
|
||
|
|
});
|