Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
verify:
name: Verify
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build (regenerates MCP tools)
run: yarn build

- name: Ensure working tree is clean after build
# `yarn build` regenerates tools.generated.ts and runs lint:fix. Asserting the
# whole tree is unchanged catches both stale generated output and any file the
# build would have auto-fixed (so lint issues can't be silently absorbed).
run: git diff --exit-code

- name: Lint
run: yarn lint

- name: Test
run: yarn test
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "lerna run build",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint --fix . --ext .js,.jsx,.ts,.tsx",
"test": "vitest run",
"changeset:version": "changeset version",
"changeset:publish": "changeset publish",
"publish": "run-s build lint changeset:publish"
Expand All @@ -29,7 +30,8 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^4.1.8"
},
"private": true,
"workspaces": [
Expand Down
59 changes: 59 additions & 0 deletions packages/openapi-codegen/src/normalizeAllOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';

import { normalizeAllOf } from './openapi';

describe('normalizeAllOf', () => {
it('merges plain allOf object members into a single object schema', () => {
const out = normalizeAllOf({
allOf: [
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
{ type: 'object', properties: { b: { type: 'number' } }, required: ['b'] },
],
} as never) as Record<string, never>;

expect((out as Record<string, unknown>).allOf).toBeUndefined();
expect(out.properties).toHaveProperty('a');
expect(out.properties).toHaveProperty('b');
expect(((out as Record<string, string[]>).required ?? []).sort()).toEqual(['a', 'b']);
});

// Regression for #34: taskCreate combined an allOf base ({content, contentType})
// with an anyOf whose branches used additionalProperties:false (.strict()). The
// strict branch rejected the sibling content/contentType, causing -32602.
// normalizeAllOf must distribute the siblings into every branch.
it('distributes sibling properties into strict anyOf branches (#34)', () => {
const out = normalizeAllOf({
allOf: [
{
type: 'object',
properties: { content: { type: 'string' }, contentType: { type: 'string' } },
required: ['content', 'contentType'],
},
{
anyOf: [
{
type: 'object',
properties: { placement: { type: 'string' } },
additionalProperties: false,
},
],
},
],
} as never) as Record<string, unknown>;

expect(out.allOf).toBeUndefined();
expect(Array.isArray(out.anyOf)).toBe(true);

const branch = (out.anyOf as Array<{ properties: Record<string, unknown> }>)[0];
// The strict branch now also knows about the sibling keys, so a payload
// carrying content + contentType + placement validates instead of being rejected.
expect(branch.properties).toHaveProperty('content');
expect(branch.properties).toHaveProperty('contentType');
expect(branch.properties).toHaveProperty('placement');
});

it('returns non-allOf schemas unchanged', () => {
const schema = { type: 'object', properties: { x: { type: 'string' } } };
expect(normalizeAllOf(schema as never)).toEqual(schema);
});
});
36 changes: 36 additions & 0 deletions packages/openapi-codegen/src/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';

import { prepareToolCallOperation } from './runtime';

describe('prepareToolCallOperation', () => {
it('splits input into path params, query params, and JSON body', () => {
const result = prepareToolCallOperation({
name: 'taskCreate',
path: '/projects/{projectId}/tasks/',
method: 'POST',
input: { projectId: 'p1', limit: 10, content: 'hello' },
pathParamKeys: ['projectId'],
queryParamKeys: ['limit'],
});

expect(result.url).toBe('/projects/p1/tasks/?limit=10');
expect(result.method).toBe('POST');
expect(JSON.parse(result.body as string)).toEqual({ content: 'hello' });
expect(result.headers['Content-Type']).toBe('application/json');
});

it('omits the body and content-type when there are no body params', () => {
const result = prepareToolCallOperation({
name: 'projectGet',
path: '/projects/{projectId}',
method: 'GET',
input: { projectId: 'p1' },
pathParamKeys: ['projectId'],
queryParamKeys: [],
});

expect(result.url).toBe('/projects/p1');
expect(result.body).toBeUndefined();
expect(result.headers['Content-Type']).toBeUndefined();
});
});
Loading
Loading