History

better openapi gen

Rhys Sullivan committed 9 months ago

Commit 4e1ea351 parent 4e0436413 files · +450 −113Browse files

Unified · Split

13 files
  1. apps/server/src/index.ts +8 −0
  2. bun.lock +40 −1
  3. packages/core/src/agent.ts +28 −14
  4. packages/core/src/discovery.ts +24 −8
  5. packages/core/src/loop.test.ts +219 −0
  6. packages/core/src/runner.ts +18 −2
  7. packages/core/src/tools.ts +19 −0
  8. packages/core/src/typechecker.ts +24 −4
  9. packages/tool-gen/package.json +1 −0
  10. packages/tool-gen/src/json-schema-to-ts.test.ts +4 −2
  11. packages/tool-gen/src/json-schema-to-ts.ts +49 −69
  12. packages/tool-gen/src/mcp.ts +5 −1
  13. packages/tool-gen/src/openapi.ts +11 −12
apps/server/src/index.ts+8−0
@@ -59,6 +59,7 @@ const mcpSources: McpToolSource[] = [ · 58 unmodified lines
59const POSTHOG_API_KEY = process.env["POSTHOG_PERSONAL_API_KEY"];59const POSTHOG_API_KEY = process.env["POSTHOG_PERSONAL_API_KEY"];
60const POSTHOG_PROJECT_ID = process.env["POSTHOG_PROJECT_ID"];60const POSTHOG_PROJECT_ID = process.env["POSTHOG_PROJECT_ID"];
61const GITHUB_TOKEN = process.env["OPENASSISTANT_GITHUB_TOKEN"];61const GITHUB_TOKEN = process.env["OPENASSISTANT_GITHUB_TOKEN"];
62const VERCEL_TOKEN = process.env["VERCEL_TOKEN"];
62 63
63const openApiSources: OpenApiToolSource[] = [64const openApiSources: OpenApiToolSource[] = [
64 ...(POSTHOG_API_KEY ? [{65 ...(POSTHOG_API_KEY ? [{
@@ -76,6 +77,13 @@ const openApiSources: OpenApiToolSource[] = [ · 11 unmodified lines
76 defaultReadApproval: "auto" as const,77 defaultReadApproval: "auto" as const,
77 defaultWriteApproval: "required" as const,78 defaultWriteApproval: "required" as const,
78 }] : []),79 }] : []),
80 ...(VERCEL_TOKEN ? [{
81 name: "vercel",
82 spec: "https://openapi.vercel.sh/",
83 auth: { type: "bearer" as const, token: VERCEL_TOKEN },
84 defaultReadApproval: "auto" as const,
85 defaultWriteApproval: "required" as const,
86 }] : []),
79];87];
80 88
81// ---------------------------------------------------------------------------89// ---------------------------------------------------------------------------
bun.lock+40−1
@@ -79,6 +79,7 @@ · 78 unmodified lines
79 "@modelcontextprotocol/sdk": "^1.26.0",79 "@modelcontextprotocol/sdk": "^1.26.0",
80 "@openassistant/core": "workspace:*",80 "@openassistant/core": "workspace:*",
81 "@sinclair/typebox": "^0.34.48",81 "@sinclair/typebox": "^0.34.48",
82 "openapi-typescript": "^7.10.1",
82 "yaml": "^2.8.2",83 "yaml": "^2.8.2",
83 "zod": "^4.3.6",84 "zod": "^4.3.6",
84 },85 },
@@ -179,6 +180,10 @@ · 94 unmodified lines
179 180
180 "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],181 "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
181 182
183 "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
184
185 "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
186
182 "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],187 "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
183 188
184 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],189 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
@@ -321,6 +326,12 @@ · 136 unmodified lines
321 326
322 "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],327 "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
323 328
329 "@redocly/ajv": ["@redocly/ajv@8.17.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng=="],
330
331 "@redocly/config": ["@redocly/config@0.22.2", "", {}, "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ=="],
332
333 "@redocly/openapi-core": ["@redocly/openapi-core@1.34.6", "", { "dependencies": { "@redocly/ajv": "^8.11.2", "@redocly/config": "^0.22.0", "colorette": "^1.2.0", "https-proxy-agent": "^7.0.5", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "minimatch": "^5.0.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw=="],
334
324 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],335 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
325 336
326 "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],337 "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
@@ -537,6 +548,8 @@ · 210 unmodified lines
537 548
538 "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],549 "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
539 550
551 "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
552
540 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],553 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
541 554
542 "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],555 "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -579,12 +592,16 @@ · 36 unmodified lines
579 592
580 "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],593 "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
581 594
595 "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
596
582 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],597 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
583 598
584 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],599 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
585 600
586 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],601 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
587 602
603 "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
604
588 "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],605 "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
589 606
590 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],607 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -737,6 +754,8 @@ · 146 unmodified lines
737 754
738 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],755 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
739 756
757 "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
758
740 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],759 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
741 760
742 "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],761 "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
@@ -753,6 +772,8 @@ · 10 unmodified lines
753 772
754 "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],773 "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
755 774
775 "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
776
756 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],777 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
757 778
758 "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],779 "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@@ -795,7 +816,7 @@ · 36 unmodified lines
795 816
796 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],817 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
797 818
798 "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],819 "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
799 820
800 "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],821 "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
801 822
@@ -831,12 +852,16 @@ · 29 unmodified lines
831 852
832 "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],853 "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
833 854
855 "openapi-typescript": ["openapi-typescript@7.10.1", "", { "dependencies": { "@redocly/openapi-core": "^1.34.5", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw=="],
856
834 "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],857 "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
835 858
836 "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],859 "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
837 860
838 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],861 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
839 862
863 "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
864
840 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],865 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
841 866
842 "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],867 "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
@@ -857,6 +882,8 @@ · 14 unmodified lines
857 882
858 "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],883 "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
859 884
885 "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
886
860 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],887 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
861 888
862 "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],889 "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
@@ -945,6 +972,8 @@ · 82 unmodified lines
945 972
946 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],973 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
947 974
975 "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
976
948 "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],977 "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
949 978
950 "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],979 "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -967,6 +996,8 @@ · 16 unmodified lines
967 996
968 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],997 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
969 998
999 "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
1000
970 "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],1001 "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
971 1002
972 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],1003 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -1005,6 +1036,10 @@ · 32 unmodified lines
1005 1036
1006 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],1037 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
1007 1038
1039 "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
1040
1041 "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1042
1008 "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],1043 "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
1009 1044
1010 "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],1045 "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
@@ -1025,6 +1060,8 @@ · 14 unmodified lines
1025 1060
1026 "@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="],1061 "@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="],
1027 1062
1063 "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
1064
1028 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],1065 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
1029 1066
1030 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],1067 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
@@ -1033,6 +1070,8 @@ · 2 unmodified lines
1033 1070
1034 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],1071 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1035 1072
1073 "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1074
1036 "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],1075 "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
1037 1076
1038 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],1077 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
packages/core/src/agent.ts+28−14
@@ -187,19 +187,32 @@ function buildSystemPrompt(guidance: string, discoveryMode: boolean): string { · 186 unmodified lines
187## Tool Discovery187## Tool Discovery
188 188
189Some tool namespaces are too large to list here. Use \`tools.discover({ query: "..." })\` to search for tools by keyword.189Some tool namespaces are too large to list here. Use \`tools.discover({ query: "..." })\` to search for tools by keyword.
190The discover tool returns an array of matching tools with their paths, descriptions, and TypeScript signatures.190It returns matching tool paths, descriptions, and TypeScript signatures.
191 191
192**Workflow for large APIs:**192**Workflow:** First call discover to learn what tools are available. Then write a SINGLE self-contained script that does the entire task — fetching data, looping, transforming, and writing back. Don't split the actual work across multiple run_code calls.
1931. Call \`tools.discover({ query: "relevant keywords" })\` to find relevant tools
1942. Read the returned signatures to understand the input/output types
1953. Call the discovered tools using the path from the results, e.g. \`tools.posthog.feature_flags.feature_flags_list({ project_id: "123" })\`
1964. You can call discover multiple times with different queries
197 193
198Example:194Example — "close all open issues on acme/myapp":
195
196Call 1 (discovery):
197\`\`\`ts
198return await tools.discover({ query: "issues list update repo" });
199\`\`\`
200
201Call 2 (do the work — everything in one script):
199\`\`\`ts202\`\`\`ts
200const found = await tools.discover({ query: "feature flags" });203const issues = await tools.github.issues.issues_list_for_repo({
201// found.results = [{ path: "posthog.feature_flags.feature_flags_list", signature: "...", ... }]204 owner: "acme", repo: "myapp", state: "open", per_page: 100
202const flags = await tools.posthog.feature_flags.feature_flags_list({ project_id: "123" });205});
206
207const results = [];
208for (const issue of issues) {
209 const closed = await tools.github.issues.issues_update({
210 owner: "acme", repo: "myapp", issue_number: issue.number, state: "closed"
211 });
212 results.push({ number: issue.number, title: issue.title, state: closed.state });
213}
214
215return { closed: results.length, issues: results };
203\`\`\`216\`\`\`
204`217`
205 : "";218 : "";
@@ -215,11 +228,12 @@ ${discoveryInstructions} · 9 unmodified lines
215## Instructions228## Instructions
216 229
217- Use the \`run_code\` tool to execute TypeScript code230- Use the \`run_code\` tool to execute TypeScript code
231- **Write complete, self-contained scripts.** Once you know what tools are available, do all the work in a single run_code call — fetch data, loop, transform, write back, and return results. Don't do reads in one call and writes in another.
218- The code runs in a sandboxed environment — only \`tools.*\` calls are available232- The code runs in a sandboxed environment — only \`tools.*\` calls are available
219- No \`fetch\`, \`process\`, \`require\`, or \`import\` — use tools for all external interactions233- No \`fetch\`, \`process\`, \`require\`, or \`import\` — use tools for all external interactions
220- Write clean, straightforward TypeScript234- Write clean, straightforward TypeScript — use for loops, map, filter, Promise.all as needed
221- Handle errors gracefully — if a tool call is denied, continue with remaining work235- Handle errors gracefully with try/catch — if a tool call fails or is denied, continue with remaining work
222- After execution, summarize what happened based on the tool call receipts236- Return a structured result from your script, then summarize what happened
223- Be concise and accurate — base your response on actual tool results, not assumptions`;237- Be concise and accurate — base your response on actual tool results, not assumptions`;
224}238}
225 239
@@ -236,7 +250,7 @@ export function createAgent(options: AgentOptions): { · 10 unmodified lines
236 requestApproval,250 requestApproval,
237 onEvent,251 onEvent,
238 maxTypecheckRetries = 3,252 maxTypecheckRetries = 3,
239 maxCodeRuns = 10,253 maxCodeRuns = 100,
240 timeoutMs = 30_000,254 timeoutMs = 30_000,
241 discoveryThreshold = DISCOVERY_THRESHOLD,255 discoveryThreshold = DISCOVERY_THRESHOLD,
242 } = options;256 } = options;
packages/core/src/discovery.ts+24−8
@@ -12,7 +12,7 @@ · 11 unmodified lines
12 */12 */
13 13
14import { defineTool, isToolDefinition, type ToolDefinition, type ToolTree } from "./tools.js";14import { defineTool, isToolDefinition, type ToolDefinition, type ToolTree } from "./tools.js";
15import { zodToTypeString } from "./typechecker.js";15import { getArgsTypeString, getReturnsTypeString } from "./typechecker.js";
16import { z } from "zod";16import { z } from "zod";
17 17
18// ---------------------------------------------------------------------------18// ---------------------------------------------------------------------------
@@ -44,8 +44,8 @@ export function buildToolIndex(tree: ToolTree): ToolIndexEntry[] { · 25 unmodified lines
44 const path = prefix ? `${prefix}.${key}` : key;44 const path = prefix ? `${prefix}.${key}` : key;
45 if (isToolDefinition(value)) {45 if (isToolDefinition(value)) {
46 const tool = value as ToolDefinition;46 const tool = value as ToolDefinition;
47 const argsType = zodToTypeString(tool.args);47 const argsType = getArgsTypeString(tool);
48 const returnsType = zodToTypeString(tool.returns);48 const returnsType = getReturnsTypeString(tool);
49 entries.push({49 entries.push({
50 path,50 path,
51 description: tool.description,51 description: tool.description,
@@ -68,8 +68,10 @@ export function buildToolIndex(tree: ToolTree): ToolIndexEntry[] { · 16 unmodified lines
68// ---------------------------------------------------------------------------68// ---------------------------------------------------------------------------
69 69
70/**70/**
71 * Simple keyword search. Matches entries where ALL query terms71 * Keyword search with fuzzy matching. Scores entries by how many
72 * appear somewhere in the path + description.72 * query terms match, with bonuses for path matches. An entry only
73 * needs to match at least half the terms (rounded up) to be included,
74 * so "github issues list update" finds tools matching any 2+ of those words.
73 */75 */
74function searchIndex(76function searchIndex(
75 index: ToolIndexEntry[],77 index: ToolIndexEntry[],
@@ -79,17 +81,31 @@ function searchIndex( · 3 unmodified lines
79 const terms = query.toLowerCase().split(/\s+/).filter(Boolean);81 const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
80 if (terms.length === 0) return index.slice(0, limit);82 if (terms.length === 0) return index.slice(0, limit);
81 83
84 // Minimum terms that must match: at least 1, or half rounded up for 3+ terms
85 const minMatches = terms.length <= 2 ? 1 : Math.ceil(terms.length / 2);
86
82 const scored = index87 const scored = index
83 .map((entry) => {88 .map((entry) => {
84 let score = 0;89 let score = 0;
90 let matched = 0;
91
85 for (const term of terms) {92 for (const term of terms) {
86 if (!entry.searchText.includes(term)) return { entry, score: -1 };93 const inText = entry.searchText.includes(term);
94 const inPath = entry.path.toLowerCase().includes(term);
95
96 if (!inText && !inPath) continue;
97
98 matched++;
99 score += 1;
87 // Bonus for path match (more specific)100 // Bonus for path match (more specific)
88 if (entry.path.toLowerCase().includes(term)) score += 2;101 if (inPath) score += 2;
89 // Bonus for exact word boundary match102 // Bonus for exact word boundary match
90 if (entry.searchText.includes(` ${term}`) || entry.searchText.startsWith(term)) score += 1;103 if (entry.searchText.includes(` ${term}`) || entry.searchText.startsWith(term)) score += 1;
91 score += 1;
92 }104 }
105
106 if (matched < minMatches) return { entry, score: -1 };
107 // Bonus for matching more terms
108 score += matched * 2;
93 return { entry, score };109 return { entry, score };
94 })110 })
95 .filter((s) => s.score > 0)111 .filter((s) => s.score > 0)
packages/core/src/loop.test.ts+219−0
@@ -0,0 +1,219 @@
1/**
2 * Test: sandbox loop behavior — list items, then mutate each in a for loop.
3 * Reproduces the "works for 1, breaks for many" issue.
4 */
5
6import { test, expect, describe } from "bun:test";
7import { createRunner } from "./runner.js";
8import { defineTool, type ToolTree } from "./tools.js";
9import { z } from "zod";
10
11// Simulate an API: list items, then update each
12const items = [
13 { id: 1, title: "Issue 1", state: "closed" },
14 { id: 2, title: "Issue 2", state: "closed" },
15 { id: 3, title: "Issue 3", state: "closed" },
16 { id: 4, title: "Issue 4", state: "closed" },
17 { id: 5, title: "Issue 5", state: "closed" },
18];
19
20let updateCallCount = 0;
21
22const tools: ToolTree = {
23 api: {
24 list_items: defineTool({
25 description: "List items",
26 approval: "auto",
27 args: z.object({ state: z.string().optional() }),
28 returns: z.array(z.object({ id: z.number(), title: z.string(), state: z.string() })),
29 run: async (input) => {
30 const filtered = input.state
31 ? items.filter((i) => i.state === input.state)
32 : items;
33 return filtered;
34 },
35 }),
36 update_item: defineTool({
37 description: "Update an item",
38 approval: "auto",
39 args: z.object({ id: z.number(), state: z.string() }),
40 returns: z.object({ id: z.number(), state: z.string() }),
41 run: async (input) => {
42 updateCallCount++;
43 // Simulate a small network delay
44 await new Promise((r) => setTimeout(r, 10));
45 return { id: input.id, state: input.state };
46 },
47 }),
48 },
49};
50
51describe("sandbox loop — list then mutate each", () => {
52 test("for loop with await works for multiple items", async () => {
53 updateCallCount = 0;
54
55 const runner = createRunner({
56 tools,
57 requestApproval: async () => "approved",
58 timeoutMs: 30_000,
59 });
60
61 const code = `
62const items = await tools.api.list_items({ state: "closed" });
63
64const results = [];
65for (const item of items) {
66 const updated = await tools.api.update_item({ id: item.id, state: "open" });
67 results.push(updated);
68}
69
70return { total: results.length, results };
71`;
72
73 const result = await runner.run(code);
74
75 console.log("Result:", JSON.stringify(result.value));
76 console.log("OK:", result.ok);
77 console.log("Error:", result.error);
78 console.log("Receipts:", result.receipts.length);
79 console.log("Update calls:", updateCallCount);
80
81 expect(result.ok).toBe(true);
82 expect(result.receipts.length).toBe(6); // 1 list + 5 updates
83 expect(updateCallCount).toBe(5);
84 expect((result.value as any).total).toBe(5);
85 });
86
87 test("Promise.all with map works for multiple items", async () => {
88 updateCallCount = 0;
89
90 const runner = createRunner({
91 tools,
92 requestApproval: async () => "approved",
93 timeoutMs: 30_000,
94 });
95
96 const code = `
97const items = await tools.api.list_items({ state: "closed" });
98
99const results = await Promise.all(
100 items.map((item) => tools.api.update_item({ id: item.id, state: "open" }))
101);
102
103return { total: results.length, results };
104`;
105
106 const result = await runner.run(code);
107
108 console.log("Result:", JSON.stringify(result.value));
109 console.log("OK:", result.ok);
110 console.log("Error:", result.error);
111
112 expect(result.ok).toBe(true);
113 expect(result.receipts.length).toBe(6);
114 expect(updateCallCount).toBe(5);
115 });
116
117 test("for loop with approval-required tools", async () => {
118 const approvalTools: ToolTree = {
119 api: {
120 list_items: defineTool({
121 description: "List items",
122 approval: "auto",
123 args: z.object({}),
124 returns: z.array(z.object({ id: z.number() })),
125 run: async () => [{ id: 1 }, { id: 2 }, { id: 3 }],
126 }),
127 update_item: defineTool({
128 description: "Update item",
129 approval: "required",
130 args: z.object({ id: z.number(), state: z.string() }),
131 returns: z.object({ id: z.number(), state: z.string() }),
132 run: async (input) => ({ id: input.id, state: input.state }),
133 }),
134 },
135 };
136
137 const runner = createRunner({
138 tools: approvalTools,
139 requestApproval: async () => "approved",
140 timeoutMs: 30_000,
141 });
142
143 const code = `
144const items = await tools.api.list_items({});
145const results = [];
146for (const item of items) {
147 const updated = await tools.api.update_item({ id: item.id, state: "open" });
148 results.push(updated);
149}
150return { total: results.length, results };
151`;
152
153 const result = await runner.run(code);
154
155 console.log("Approval loop result:", JSON.stringify(result.value));
156 console.log("OK:", result.ok);
157 console.log("Error:", result.error);
158 console.log("Receipts:", result.receipts.length);
159
160 expect(result.ok).toBe(true);
161 expect(result.receipts.length).toBe(4); // 1 list + 3 updates
162 expect((result.value as any).total).toBe(3);
163 });
164
165 test("large loop — 20 items", async () => {
166 const manyItems = Array.from({ length: 20 }, (_, i) => ({
167 id: i + 1,
168 title: `Issue ${i + 1}`,
169 state: "closed",
170 }));
171
172 const manyTools: ToolTree = {
173 api: {
174 list_items: defineTool({
175 description: "List items",
176 approval: "auto",
177 args: z.object({}),
178 returns: z.any(),
179 run: async () => manyItems,
180 }),
181 update_item: defineTool({
182 description: "Update item",
183 approval: "auto",
184 args: z.object({ id: z.number(), state: z.string() }),
185 returns: z.object({ id: z.number(), state: z.string() }),
186 run: async (input) => {
187 await new Promise((r) => setTimeout(r, 5));
188 return { id: input.id, state: input.state };
189 },
190 }),
191 },
192 };
193
194 const runner = createRunner({
195 tools: manyTools,
196 requestApproval: async () => "approved",
197 timeoutMs: 30_000,
198 });
199
200 const code = `
201const items = await tools.api.list_items({});
202const results = [];
203for (const item of items) {
204 const updated = await tools.api.update_item({ id: item.id, state: "open" });
205 results.push(updated);
206}
207return { total: results.length };
208`;
209
210 const result = await runner.run(code);
211
212 console.log("20 items result:", JSON.stringify(result.value));
213 console.log("OK:", result.ok);
214 console.log("Error:", result.error);
215
216 expect(result.ok).toBe(true);
217 expect((result.value as any).total).toBe(20);
218 });
219});
packages/core/src/runner.ts+18−2
@@ -66,10 +66,26 @@ function preview(value: unknown): string { · 65 unmodified lines
66}66}
67 67
68function describeError(error: unknown): string {68function describeError(error: unknown): string {
69 if (error instanceof Error) return error.message;69 if (error instanceof Error) {
70 // Include cause if present (e.g. nested API errors)
71 const msg = error.message;
72 if (error.cause) {
73 return `${msg} (cause: ${describeError(error.cause)})`;
74 }
75 return msg;
76 }
70 if (typeof error === "string") return error;77 if (typeof error === "string") return error;
78 if (error === null || error === undefined) return String(error);
79 // Try to get a meaningful representation
71 try {80 try {
72 return JSON.stringify(error);81 const json = JSON.stringify(error, null, 0);
82 // If it's just "{}", try to find something useful
83 if (json === "{}" || json === "[]") {
84 const proto = Object.getPrototypeOf(error);
85 const name = proto?.constructor?.name;
86 return name && name !== "Object" ? `[${name}]` : String(error);
87 }
88 return json;
73 } catch {89 } catch {
74 return String(error);90 return String(error);
75 }91 }
packages/core/src/tools.ts+19−0
@@ -62,6 +62,12 @@ export interface ApprovalRequest { · 61 unmodified lines
62 * - TypeScript declaration generation for the typechecker62 * - TypeScript declaration generation for the typechecker
63 * - LLM prompt guidance generation63 * - LLM prompt guidance generation
64 *64 *
65 * - `metadata.argsType` and `metadata.returnsType` are optional pre-computed
66 * TypeScript type strings. When present, the typechecker and discovery index
67 * use these instead of deriving types from Zod schemas. This is critical for
68 * OpenAPI tools where `returns` is `z.any()` but the spec defines a rich
69 * response schema.
70 *
65 * - `run` is a plain async function (Effect is an internal detail)71 * - `run` is a plain async function (Effect is an internal detail)
66 *72 *
67 * - `formatApproval` provides rich presentation for approval UIs73 * - `formatApproval` provides rich presentation for approval UIs
@@ -79,6 +85,13 @@ export interface ToolDefinition< · 11 unmodified lines
79 readonly formatApproval?: ((85 readonly formatApproval?: ((
80 input: z.output<TArgs>,86 input: z.output<TArgs>,
81 ) => ApprovalPresentation) | undefined;87 ) => ApprovalPresentation) | undefined;
88 /** Pre-computed type metadata. Preferred over Zod-derived types when present. */
89 readonly metadata?: {
90 /** TypeScript type string for the input args (e.g. "{ owner: string; repo: string }") */
91 readonly argsType?: string | undefined;
92 /** TypeScript type string for the return value (e.g. "{ id: number; title: string }") */
93 readonly returnsType?: string | undefined;
94 } | undefined;
82}95}
83 96
84/**97/**
@@ -102,6 +115,11 @@ export interface DefineToolInput< · 17 unmodified lines
102 returns: TReturns;115 returns: TReturns;
103 run: (input: z.output<TArgs>) => Promise<z.output<TReturns>>;116 run: (input: z.output<TArgs>) => Promise<z.output<TReturns>>;
104 formatApproval?: (input: z.output<TArgs>) => ApprovalPresentation;117 formatApproval?: (input: z.output<TArgs>) => ApprovalPresentation;
118 /** Pre-computed type metadata. Preferred over Zod-derived types when present. */
119 metadata?: {
120 argsType?: string;
121 returnsType?: string;
122 };
105}123}
106 124
107/**125/**
@@ -131,6 +149,7 @@ export function defineTool< · 23 unmodified lines
131 returns: input.returns,149 returns: input.returns,
132 run: input.run,150 run: input.run,
133 formatApproval: input.formatApproval,151 formatApproval: input.formatApproval,
152 metadata: input.metadata,
134 };153 };
135}154}
136 155
packages/core/src/typechecker.ts+24−4
@@ -125,6 +125,26 @@ export function zodToTypeString(schema: z.ZodType): string { · 124 unmodified lines
125 }125 }
126}126}
127 127
128// ---------------------------------------------------------------------------
129// Type string resolution
130// ---------------------------------------------------------------------------
131
132/**
133 * Get the TypeScript type string for a tool's args.
134 * Prefers metadata.argsType when available, falls back to Zod-derived type.
135 */
136export function getArgsTypeString(tool: ToolDefinition): string {
137 return tool.metadata?.argsType ?? zodToTypeString(tool.args);
138}
139
140/**
141 * Get the TypeScript type string for a tool's return value.
142 * Prefers metadata.returnsType when available, falls back to Zod-derived type.
143 */
144export function getReturnsTypeString(tool: ToolDefinition): string {
145 return tool.metadata?.returnsType ?? zodToTypeString(tool.returns);
146}
147
128// ---------------------------------------------------------------------------148// ---------------------------------------------------------------------------
129// ToolTree → TypeScript declaration149// ToolTree → TypeScript declaration
130// ---------------------------------------------------------------------------150// ---------------------------------------------------------------------------
@@ -144,8 +164,8 @@ function generateTreeType(tree: ToolTree, indent: number): string { · 13 unmodified lines
144 for (const [key, value] of Object.entries(tree)) {164 for (const [key, value] of Object.entries(tree)) {
145 if (isToolDefinition(value)) {165 if (isToolDefinition(value)) {
146 const tool = value as ToolDefinition;166 const tool = value as ToolDefinition;
147 const argsType = zodToTypeString(tool.args);167 const argsType = getArgsTypeString(tool);
148 const returnsType = zodToTypeString(tool.returns);168 const returnsType = getReturnsTypeString(tool);
149 lines.push(`${pad}${key}(input: ${argsType}): Promise<${returnsType}>;`);169 lines.push(`${pad}${key}(input: ${argsType}): Promise<${returnsType}>;`);
150 } else {170 } else {
151 lines.push(`${pad}${key}: {`);171 lines.push(`${pad}${key}: {`);
@@ -173,8 +193,8 @@ export function generatePromptGuidance(tree: ToolTree): string { · 21 unmodified lines
173 const path = prefix ? `${prefix}.${key}` : key;193 const path = prefix ? `${prefix}.${key}` : key;
174 if (isToolDefinition(value)) {194 if (isToolDefinition(value)) {
175 const tool = value as ToolDefinition;195 const tool = value as ToolDefinition;
176 const argsType = zodToTypeString(tool.args);196 const argsType = getArgsTypeString(tool);
177 const returnsType = zodToTypeString(tool.returns);197 const returnsType = getReturnsTypeString(tool);
178 const approvalNote =198 const approvalNote =
179 tool.approval === "required" ? " (approval required)" : " (auto-approved)";199 tool.approval === "required" ? " (approval required)" : " (auto-approved)";
180 lines.push(200 lines.push(
packages/tool-gen/package.json+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
17 "@modelcontextprotocol/sdk": "^1.26.0",17 "@modelcontextprotocol/sdk": "^1.26.0",
18 "@openassistant/core": "workspace:*",18 "@openassistant/core": "workspace:*",
19 "@sinclair/typebox": "^0.34.48",19 "@sinclair/typebox": "^0.34.48",
20 "openapi-typescript": "^7.10.1",
20 "yaml": "^2.8.2",21 "yaml": "^2.8.2",
21 "zod": "^4.3.6"22 "zod": "^4.3.6"
22 }23 }
packages/tool-gen/src/json-schema-to-ts.test.ts+4−2
@@ -26,7 +26,8 @@ describe("jsonSchemaToTypeString", () => { · 25 unmodified lines
26 26
27 test("object with no properties", () => {27 test("object with no properties", () => {
28 const result = jsonSchemaToTypeString({ type: "object" });28 const result = jsonSchemaToTypeString({ type: "object" });
29 expect(result).toBe("Record<string, unknown>");29 // openapi-typescript produces Record<string, never> for empty objects
30 expect(result).toBe("Record<string, never>");
30 });31 });
31 32
32 test("array", () => {33 test("array", () => {
@@ -34,7 +35,8 @@ describe("jsonSchemaToTypeString", () => { · 1 unmodified line
34 type: "array",35 type: "array",
35 items: { type: "string" },36 items: { type: "string" },
36 };37 };
37 expect(jsonSchemaToTypeString(schema)).toBe("Array<string>");38 // openapi-typescript uses T[] syntax instead of Array<T>
39 expect(jsonSchemaToTypeString(schema)).toBe("string[]");
38 });40 });
39 41
40 test("enum", () => {42 test("enum", () => {
packages/tool-gen/src/json-schema-to-ts.ts+49−69
@@ -4,10 +4,42 @@ · 3 unmodified lines
4 * Both MCP tools and OpenAPI specs use JSON Schema for their type definitions.4 * Both MCP tools and OpenAPI specs use JSON Schema for their type definitions.
5 * This module converts them to TypeScript type strings for the typechecker,5 * This module converts them to TypeScript type strings for the typechecker,
6 * and also creates Zod schemas for runtime validation.6 * and also creates Zod schemas for runtime validation.
7 *
8 * Type string generation uses openapi-typescript's `transformSchemaObject` for
9 * high-fidelity conversion (handles allOf, oneOf, enums, nullable, etc.).
10 * Falls back to our simpler hand-rolled converter for edge cases.
7 */11 */
8 12
13import { astToString, transformSchemaObject } from "openapi-typescript";
9import { z } from "zod";14import { z } from "zod";
10 15
16// ---------------------------------------------------------------------------
17// openapi-typescript context (for individual schema conversion)
18// ---------------------------------------------------------------------------
19
20function makeTransformCtx() {
21 return {
22 additionalProperties: false,
23 alphabetize: false,
24 arrayLength: false,
25 defaultNonNullable: true,
26 discriminators: { refsHandled: [] as string[], objects: {} as Record<string, unknown> },
27 emptyObjectsUnknown: false,
28 enum: false,
29 enumValues: false,
30 excludeDeprecated: false,
31 exportType: false,
32 immutable: false,
33 indentLv: 0,
34 pathParamsAsTypes: false,
35 postTransform: undefined,
36 propertiesRequiredByDefault: false,
37 redoc: undefined,
38 silent: true,
39 resolve(_ref: string) { return undefined as unknown; },
40 };
41}
42
11// ---------------------------------------------------------------------------43// ---------------------------------------------------------------------------
12// JSON Schema types (minimal subset we need)44// JSON Schema types (minimal subset we need)
13// ---------------------------------------------------------------------------45// ---------------------------------------------------------------------------
@@ -29,81 +61,29 @@ export interface JsonSchema { · 15 unmodified lines
29}61}
30 62
31// ---------------------------------------------------------------------------63// ---------------------------------------------------------------------------
32// JSON Schema → TypeScript string64// JSON Schema → TypeScript string (via openapi-typescript)
33// ---------------------------------------------------------------------------65// ---------------------------------------------------------------------------
34 66
67/**
68 * Convert a JSON Schema to a TypeScript type string.
69 *
70 * Uses openapi-typescript's `transformSchemaObject` for high-fidelity conversion
71 * that handles allOf, oneOf, enums, nullable, additionalProperties, etc.
72 * Falls back to "any" for schemas that can't be converted (e.g. circular refs).
73 */
35export function jsonSchemaToTypeString(schema: JsonSchema): string {74export function jsonSchemaToTypeString(schema: JsonSchema): string {
36 if (!schema.type && schema.properties) {75 if (!schema) return "any";
37 // Implicit object type76 try {
38 return objectToTypeString(schema);77 const node = transformSchemaObject(schema as never, {
39 }78 path: "#",
40 79 ctx: makeTransformCtx() as never,
41 if (schema.enum) {80 });
42 return schema.enum81 return astToString(node).trim();
43 .map((v) => (typeof v === "string" ? `"${v}"` : String(v)))82 } catch {
44 .join(" | ");83 return "any";
45 }
46
47 if (schema.const !== undefined) {
48 return typeof schema.const === "string"
49 ? `"${schema.const}"`
50 : String(schema.const);
51 }
52
53 if (schema.oneOf) {
54 return schema.oneOf.map(jsonSchemaToTypeString).join(" | ");
55 }
56
57 if (schema.anyOf) {
58 return schema.anyOf.map(jsonSchemaToTypeString).join(" | ");
59 }
60
61 const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
62
63 switch (type) {
64 case "string":
65 return "string";
66 case "number":
67 case "integer":
68 return "number";
69 case "boolean":
70 return "boolean";
71 case "null":
72 return "null";
73 case "array": {
74 if (schema.items) {
75 return `Array<${jsonSchemaToTypeString(schema.items)}>`;
76 }
77 return "Array<unknown>";
78 }
79 case "object":
80 return objectToTypeString(schema);
81 default:
82 return "unknown";
83 }84 }
84}85}
85 86
86function objectToTypeString(schema: JsonSchema): string {
87 const props = schema.properties;
88 if (!props || Object.keys(props).length === 0) {
89 if (schema.additionalProperties) {
90 const valueType =
91 typeof schema.additionalProperties === "object"
92 ? jsonSchemaToTypeString(schema.additionalProperties)
93 : "unknown";
94 return `Record<string, ${valueType}>`;
95 }
96 return "Record<string, unknown>";
97 }
98
99 const required = new Set(schema.required ?? []);
100 const entries = Object.entries(props).map(([key, propSchema]) => {
101 const opt = required.has(key) ? "" : "?";
102 return `${key}${opt}: ${jsonSchemaToTypeString(propSchema)}`;
103 });
104 return `{ ${entries.join("; ")} }`;
105}
106
107// ---------------------------------------------------------------------------87// ---------------------------------------------------------------------------
108// JSON Schema → Zod schema88// JSON Schema → Zod schema
109// ---------------------------------------------------------------------------89// ---------------------------------------------------------------------------
packages/tool-gen/src/mcp.ts+5−1
@@ -116,7 +116,7 @@ export async function generateMcpTools( · 115 unmodified lines
116 // Generate Zod schema from JSON Schema116 // Generate Zod schema from JSON Schema
117 const argsZod = jsonSchemaToZod(inputSchema);117 const argsZod = jsonSchemaToZod(inputSchema);
118 118
119 // Generate TypeScript type string for declarations119 // Generate TypeScript type string using openapi-typescript
120 const argsTypeString = jsonSchemaToTypeString(inputSchema);120 const argsTypeString = jsonSchemaToTypeString(inputSchema);
121 121
122 // Create the tool definition122 // Create the tool definition
@@ -125,6 +125,10 @@ export async function generateMcpTools( · 2 unmodified lines
125 approval,125 approval,
126 args: argsZod,126 args: argsZod,
127 returns: z.any(), // MCP tools return opaque content127 returns: z.any(), // MCP tools return opaque content
128 metadata: {
129 argsType: argsTypeString,
130 // MCP tools don't declare output schemas, so no returnsType
131 },
128 run: async (input: unknown) => {132 run: async (input: unknown) => {
129 const result = await client.callTool({133 const result = await client.callTool({
130 name: toolName,134 name: toolName,
packages/tool-gen/src/openapi.ts+11−12
@@ -82,6 +82,7 @@ interface ParsedParameter { · 81 unmodified lines
82 82
83const READ_METHODS = new Set(["get", "head", "options"]);83const READ_METHODS = new Set(["get", "head", "options"]);
84 84
85
85function buildAuthHeaders(auth: OpenApiAuth | undefined): Record<string, string> {86function buildAuthHeaders(auth: OpenApiAuth | undefined): Record<string, string> {
86 if (!auth) return {};87 if (!auth) return {};
87 switch (auth.type) {88 switch (auth.type) {
@@ -305,25 +306,19 @@ export async function generateOpenApiTools( · 217 unmodified lines
305 };306 };
306 307
307 let argsZod: z.ZodType;308 let argsZod: z.ZodType;
308 let argsTypeString: string;
309 let returnsTypeString: string;
310 309
311 try {310 try {
312 argsZod = jsonSchemaToZod(combinedInputSchema);311 argsZod = jsonSchemaToZod(combinedInputSchema);
313 argsTypeString = jsonSchemaToTypeString(combinedInputSchema);
314 } catch {312 } catch {
315 // Recursive/circular schemas — fall back to z.any()313 // Recursive/circular schemas — fall back to z.any()
316 argsZod = z.any();314 argsZod = z.any();
317 argsTypeString = "any";
318 }315 }
319 316
320 try {317 // Use openapi-typescript for type strings (handles allOf, oneOf, enums, etc.)
321 returnsTypeString = op.responseSchema318 const argsTypeString = jsonSchemaToTypeString(combinedInputSchema);
322 ? jsonSchemaToTypeString(op.responseSchema)319 const returnsTypeString = op.responseSchema
323 : "any";320 ? jsonSchemaToTypeString(op.responseSchema)
324 } catch {321 : "any";
325 returnsTypeString = "any";
326 }
327 322
328 const description = op.summary || op.description || `${op.method.toUpperCase()} ${op.path}`;323 const description = op.summary || op.description || `${op.method.toUpperCase()} ${op.path}`;
329 324
@@ -332,6 +327,10 @@ export async function generateOpenApiTools( · 2 unmodified lines
332 approval,327 approval,
333 args: argsZod,328 args: argsZod,
334 returns: z.any(), // OpenAPI responses are complex; use any for runtime329 returns: z.any(), // OpenAPI responses are complex; use any for runtime
330 metadata: {
331 argsType: argsTypeString,
332 returnsType: returnsTypeString,
333 },
335 run: async (input: unknown) => {334 run: async (input: unknown) => {
336 const { url, remainingInput } = buildUrl(335 const { url, remainingInput } = buildUrl(
337 baseUrl,336 baseUrl,
@@ -358,7 +357,7 @@ export async function generateOpenApiTools( · 20 unmodified lines
358 357
359 if (!response.ok) {358 if (!response.ok) {
360 const text = await response.text().catch(() => "");359 const text = await response.text().catch(() => "");
361 throw new Error(`${response.status} ${response.statusText}: ${text.slice(0, 500)}`);360 throw new Error(`HTTP ${response.status} ${response.statusText} (${op.method.toUpperCase()} ${url}): ${text.slice(0, 500)}`);
362 }361 }
363 362
364 const contentType = response.headers.get("content-type") ?? "";363 const contentType = response.headers.get("content-type") ?? "";