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
5959 const POSTHOG_API_KEY = process.env["POSTHOG_PERSONAL_API_KEY"];
6060 const POSTHOG_PROJECT_ID = process.env["POSTHOG_PROJECT_ID"];
6161 const GITHUB_TOKEN = process.env["OPENASSISTANT_GITHUB_TOKEN"];
62+const VERCEL_TOKEN = process.env["VERCEL_TOKEN"];
6263
6364 const openApiSources: OpenApiToolSource[] = [
6465 ...(POSTHOG_API_KEY ? [{
@@ -76,6 +77,13 @@ const openApiSources: OpenApiToolSource[] = [ · 11 unmodified lines
7677 defaultReadApproval: "auto" as const,
7778 defaultWriteApproval: "required" as const,
7879 }] : []),
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+ }] : []),
7987 ];
8088
8189 // ---------------------------------------------------------------------------
bun.lock+40−1
@@ -79,6 +79,7 @@ · 78 unmodified lines
7979 "@modelcontextprotocol/sdk": "^1.26.0",
8080 "@openassistant/core": "workspace:*",
8181 "@sinclair/typebox": "^0.34.48",
82+ "openapi-typescript": "^7.10.1",
8283 "yaml": "^2.8.2",
8384 "zod": "^4.3.6",
8485 },
@@ -179,6 +180,10 @@ · 94 unmodified lines
179180
180181 "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
181182
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+
182187 "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
183188
184189 "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
@@ -321,6 +326,12 @@ · 136 unmodified lines
321326
322327 "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
323328
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+
324335 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
325336
326337 "@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
537548
538549 "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
539550
551+ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
552+
540553 "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
541554
542555 "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -579,12 +592,16 @@ · 36 unmodified lines
579592
580593 "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
581594
595+ "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
596+
582597 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
583598
584599 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
585600
586601 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
587602
603+ "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
604+
588605 "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
589606
590607 "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -737,6 +754,8 @@ · 146 unmodified lines
737754
738755 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
739756
757+ "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
758+
740759 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
741760
742761 "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
@@ -753,6 +772,8 @@ · 10 unmodified lines
753772
754773 "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
755774
775+ "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
776+
756777 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
757778
758779 "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
795816
796817 "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
797818
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=="],
799820
800821 "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
801822
@@ -831,12 +852,16 @@ · 29 unmodified lines
831852
832853 "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
833854
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+
834857 "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=="],
835858
836859 "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
837860
838861 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
839862
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+
840865 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
841866
842867 "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
@@ -857,6 +882,8 @@ · 14 unmodified lines
857882
858883 "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
859884
885+ "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
886+
860887 "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=="],
861888
862889 "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
945972
946973 "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
947974
975+ "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
976+
948977 "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
949978
950979 "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -967,6 +996,8 @@ · 16 unmodified lines
967996
968997 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
969998
999+ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
1000+
9701001 "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=="],
9711002
9721003 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -1005,6 +1036,10 @@ · 32 unmodified lines
10051036
10061037 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
10071038
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+
10081043 "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
10091044
10101045 "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
10251060
10261061 "@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=="],
10271062
1063+ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
1064+
10281065 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
10291066
10301067 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
@@ -1033,6 +1070,8 @@ · 2 unmodified lines
10331070
10341071 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
10351072
1073+ "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1074+
10361075 "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
10371076
10381077 "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
187187 ## Tool Discovery
188188
189189 Some tool namespaces are too large to list here. Use \`tools.discover({ query: "..." })\` to search for tools by keyword.
190−The discover tool returns an array of matching tools with their paths, descriptions, and TypeScript signatures.
190+It returns matching tool paths, descriptions, and TypeScript signatures.
191191
192−**Workflow for large APIs:**
193−1. Call \`tools.discover({ query: "relevant keywords" })\` to find relevant tools
194−2. Read the returned signatures to understand the input/output types
195−3. Call the discovered tools using the path from the results, e.g. \`tools.posthog.feature_flags.feature_flags_list({ project_id: "123" })\`
196−4. You can call discover multiple times with different queries
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.
197193
198−Example:
194+Example — "close all open issues on acme/myapp":
195+
196+Call 1 (discovery):
197+\`\`\`ts
198+return await tools.discover({ query: "issues list update repo" });
199+\`\`\`
200+
201+Call 2 (do the work — everything in one script):
199202 \`\`\`ts
200−const found = await tools.discover({ query: "feature flags" });
201−// found.results = [{ path: "posthog.feature_flags.feature_flags_list", signature: "...", ... }]
202−const flags = await tools.posthog.feature_flags.feature_flags_list({ project_id: "123" });
203+const issues = await tools.github.issues.issues_list_for_repo({
204+ owner: "acme", repo: "myapp", state: "open", per_page: 100
205+});
206+
207+const results = [];
208+for (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+
215+return { closed: results.length, issues: results };
203216 \`\`\`
204217 `
205218 : "";
@@ -215,11 +228,12 @@ ${discoveryInstructions} · 9 unmodified lines
215228 ## Instructions
216229
217230 - 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.
218232 - The code runs in a sandboxed environment — only \`tools.*\` calls are available
219233 - No \`fetch\`, \`process\`, \`require\`, or \`import\` — use tools for all external interactions
220−- Write clean, straightforward TypeScript
221−- Handle errors gracefully — if a tool call is denied, continue with remaining work
222−- After execution, summarize what happened based on the tool call receipts
234+- Write clean, straightforward TypeScript — use for loops, map, filter, Promise.all as needed
235+- Handle errors gracefully with try/catch — if a tool call fails or is denied, continue with remaining work
236+- Return a structured result from your script, then summarize what happened
223237 - Be concise and accurate — base your response on actual tool results, not assumptions`;
224238 }
225239
@@ -236,7 +250,7 @@ export function createAgent(options: AgentOptions): { · 10 unmodified lines
236250 requestApproval,
237251 onEvent,
238252 maxTypecheckRetries = 3,
239− maxCodeRuns = 10,
253+ maxCodeRuns = 100,
240254 timeoutMs = 30_000,
241255 discoveryThreshold = DISCOVERY_THRESHOLD,
242256 } = options;
packages/core/src/discovery.ts+24−8
@@ -12,7 +12,7 @@ · 11 unmodified lines
1212 */
1313
1414 import { defineTool, isToolDefinition, type ToolDefinition, type ToolTree } from "./tools.js";
15−import { zodToTypeString } from "./typechecker.js";
15+import { getArgsTypeString, getReturnsTypeString } from "./typechecker.js";
1616 import { z } from "zod";
1717
1818 // ---------------------------------------------------------------------------
@@ -44,8 +44,8 @@ export function buildToolIndex(tree: ToolTree): ToolIndexEntry[] { · 25 unmodified lines
4444 const path = prefix ? `${prefix}.${key}` : key;
4545 if (isToolDefinition(value)) {
4646 const tool = value as ToolDefinition;
47− const argsType = zodToTypeString(tool.args);
48− const returnsType = zodToTypeString(tool.returns);
47+ const argsType = getArgsTypeString(tool);
48+ const returnsType = getReturnsTypeString(tool);
4949 entries.push({
5050 path,
5151 description: tool.description,
@@ -68,8 +68,10 @@ export function buildToolIndex(tree: ToolTree): ToolIndexEntry[] { · 16 unmodified lines
6868 // ---------------------------------------------------------------------------
6969
7070 /**
71− * Simple keyword search. Matches entries where ALL query terms
72− * appear somewhere in the path + description.
71+ * Keyword search with fuzzy matching. Scores entries by how many
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.
7375 */
7476 function searchIndex(
7577 index: ToolIndexEntry[],
@@ -79,17 +81,31 @@ function searchIndex( · 3 unmodified lines
7981 const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
8082 if (terms.length === 0) return index.slice(0, limit);
8183
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+
8287 const scored = index
8388 .map((entry) => {
8489 let score = 0;
90+ let matched = 0;
91+
8592 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;
87100 // Bonus for path match (more specific)
88− if (entry.path.toLowerCase().includes(term)) score += 2;
101+ if (inPath) score += 2;
89102 // Bonus for exact word boundary match
90103 if (entry.searchText.includes(` ${term}`) || entry.searchText.startsWith(term)) score += 1;
91− score += 1;
92104 }
105+
106+ if (matched < minMatches) return { entry, score: -1 };
107+ // Bonus for matching more terms
108+ score += matched * 2;
93109 return { entry, score };
94110 })
95111 .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+
6+import { test, expect, describe } from "bun:test";
7+import { createRunner } from "./runner.js";
8+import { defineTool, type ToolTree } from "./tools.js";
9+import { z } from "zod";
10+
11+// Simulate an API: list items, then update each
12+const 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+
20+let updateCallCount = 0;
21+
22+const 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+
51+describe("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 = `
62+const items = await tools.api.list_items({ state: "closed" });
63+
64+const results = [];
65+for (const item of items) {
66+ const updated = await tools.api.update_item({ id: item.id, state: "open" });
67+ results.push(updated);
68+}
69+
70+return { 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 = `
97+const items = await tools.api.list_items({ state: "closed" });
98+
99+const results = await Promise.all(
100+ items.map((item) => tools.api.update_item({ id: item.id, state: "open" }))
101+);
102+
103+return { 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 = `
144+const items = await tools.api.list_items({});
145+const results = [];
146+for (const item of items) {
147+ const updated = await tools.api.update_item({ id: item.id, state: "open" });
148+ results.push(updated);
149+}
150+return { 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 = `
201+const items = await tools.api.list_items({});
202+const results = [];
203+for (const item of items) {
204+ const updated = await tools.api.update_item({ id: item.id, state: "open" });
205+ results.push(updated);
206+}
207+return { 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
6666 }
6767
6868 function 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+ }
7077 if (typeof error === "string") return error;
78+ if (error === null || error === undefined) return String(error);
79+ // Try to get a meaningful representation
7180 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;
7389 } catch {
7490 return String(error);
7591 }
packages/core/src/tools.ts+19−0
@@ -62,6 +62,12 @@ export interface ApprovalRequest { · 61 unmodified lines
6262 * - TypeScript declaration generation for the typechecker
6363 * - LLM prompt guidance generation
6464 *
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+ *
6571 * - `run` is a plain async function (Effect is an internal detail)
6672 *
6773 * - `formatApproval` provides rich presentation for approval UIs
@@ -79,6 +85,13 @@ export interface ToolDefinition< · 11 unmodified lines
7985 readonly formatApproval?: ((
8086 input: z.output<TArgs>,
8187 ) => 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;
8295 }
8396
8497 /**
@@ -102,6 +115,11 @@ export interface DefineToolInput< · 17 unmodified lines
102115 returns: TReturns;
103116 run: (input: z.output<TArgs>) => Promise<z.output<TReturns>>;
104117 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+ };
105123 }
106124
107125 /**
@@ -131,6 +149,7 @@ export function defineTool< · 23 unmodified lines
131149 returns: input.returns,
132150 run: input.run,
133151 formatApproval: input.formatApproval,
152+ metadata: input.metadata,
134153 };
135154 }
136155
packages/core/src/typechecker.ts+24−4
@@ -125,6 +125,26 @@ export function zodToTypeString(schema: z.ZodType): string { · 124 unmodified lines
125125 }
126126 }
127127
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+ */
136+export 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+ */
144+export function getReturnsTypeString(tool: ToolDefinition): string {
145+ return tool.metadata?.returnsType ?? zodToTypeString(tool.returns);
146+}
147+
128148 // ---------------------------------------------------------------------------
129149 // ToolTree → TypeScript declaration
130150 // ---------------------------------------------------------------------------
@@ -144,8 +164,8 @@ function generateTreeType(tree: ToolTree, indent: number): string { · 13 unmodified lines
144164 for (const [key, value] of Object.entries(tree)) {
145165 if (isToolDefinition(value)) {
146166 const tool = value as ToolDefinition;
147− const argsType = zodToTypeString(tool.args);
148− const returnsType = zodToTypeString(tool.returns);
167+ const argsType = getArgsTypeString(tool);
168+ const returnsType = getReturnsTypeString(tool);
149169 lines.push(`${pad}${key}(input: ${argsType}): Promise<${returnsType}>;`);
150170 } else {
151171 lines.push(`${pad}${key}: {`);
@@ -173,8 +193,8 @@ export function generatePromptGuidance(tree: ToolTree): string { · 21 unmodified lines
173193 const path = prefix ? `${prefix}.${key}` : key;
174194 if (isToolDefinition(value)) {
175195 const tool = value as ToolDefinition;
176− const argsType = zodToTypeString(tool.args);
177− const returnsType = zodToTypeString(tool.returns);
196+ const argsType = getArgsTypeString(tool);
197+ const returnsType = getReturnsTypeString(tool);
178198 const approvalNote =
179199 tool.approval === "required" ? " (approval required)" : " (auto-approved)";
180200 lines.push(
packages/tool-gen/package.json+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
1717 "@modelcontextprotocol/sdk": "^1.26.0",
1818 "@openassistant/core": "workspace:*",
1919 "@sinclair/typebox": "^0.34.48",
20+ "openapi-typescript": "^7.10.1",
2021 "yaml": "^2.8.2",
2122 "zod": "^4.3.6"
2223 }
packages/tool-gen/src/json-schema-to-ts.test.ts+4−2
@@ -26,7 +26,8 @@ describe("jsonSchemaToTypeString", () => { · 25 unmodified lines
2626
2727 test("object with no properties", () => {
2828 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>");
3031 });
3132
3233 test("array", () => {
@@ -34,7 +35,8 @@ describe("jsonSchemaToTypeString", () => { · 1 unmodified line
3435 type: "array",
3536 items: { type: "string" },
3637 };
37− expect(jsonSchemaToTypeString(schema)).toBe("Array<string>");
38+ // openapi-typescript uses T[] syntax instead of Array<T>
39+ expect(jsonSchemaToTypeString(schema)).toBe("string[]");
3840 });
3941
4042 test("enum", () => {
packages/tool-gen/src/json-schema-to-ts.ts+49−69
@@ -4,10 +4,42 @@ · 3 unmodified lines
44 * Both MCP tools and OpenAPI specs use JSON Schema for their type definitions.
55 * This module converts them to TypeScript type strings for the typechecker,
66 * 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.
711 */
812
13+import { astToString, transformSchemaObject } from "openapi-typescript";
914 import { z } from "zod";
1015
16+// ---------------------------------------------------------------------------
17+// openapi-typescript context (for individual schema conversion)
18+// ---------------------------------------------------------------------------
19+
20+function 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+
1143 // ---------------------------------------------------------------------------
1244 // JSON Schema types (minimal subset we need)
1345 // ---------------------------------------------------------------------------
@@ -29,81 +61,29 @@ export interface JsonSchema { · 15 unmodified lines
2961 }
3062
3163 // ---------------------------------------------------------------------------
32−// JSON Schema → TypeScript string
64+// JSON Schema → TypeScript string (via openapi-typescript)
3365 // ---------------------------------------------------------------------------
3466
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+ */
3574 export function jsonSchemaToTypeString(schema: JsonSchema): string {
36− if (!schema.type && schema.properties) {
37− // Implicit object type
38− return objectToTypeString(schema);
39− }
40−
41− if (schema.enum) {
42− return schema.enum
43− .map((v) => (typeof v === "string" ? `"${v}"` : String(v)))
44− .join(" | ");
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";
75+ if (!schema) return "any";
76+ try {
77+ const node = transformSchemaObject(schema as never, {
78+ path: "#",
79+ ctx: makeTransformCtx() as never,
80+ });
81+ return astToString(node).trim();
82+ } catch {
83+ return "any";
8384 }
8485 }
8586
86−function 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−
10787 // ---------------------------------------------------------------------------
10888 // JSON Schema → Zod schema
10989 // ---------------------------------------------------------------------------
packages/tool-gen/src/mcp.ts+5−1
@@ -116,7 +116,7 @@ export async function generateMcpTools( · 115 unmodified lines
116116 // Generate Zod schema from JSON Schema
117117 const argsZod = jsonSchemaToZod(inputSchema);
118118
119− // Generate TypeScript type string for declarations
119+ // Generate TypeScript type string using openapi-typescript
120120 const argsTypeString = jsonSchemaToTypeString(inputSchema);
121121
122122 // Create the tool definition
@@ -125,6 +125,10 @@ export async function generateMcpTools( · 2 unmodified lines
125125 approval,
126126 args: argsZod,
127127 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+ },
128132 run: async (input: unknown) => {
129133 const result = await client.callTool({
130134 name: toolName,
packages/tool-gen/src/openapi.ts+11−12
@@ -82,6 +82,7 @@ interface ParsedParameter { · 81 unmodified lines
8282
8383 const READ_METHODS = new Set(["get", "head", "options"]);
8484
85+
8586 function buildAuthHeaders(auth: OpenApiAuth | undefined): Record<string, string> {
8687 if (!auth) return {};
8788 switch (auth.type) {
@@ -305,25 +306,19 @@ export async function generateOpenApiTools( · 217 unmodified lines
305306 };
306307
307308 let argsZod: z.ZodType;
308− let argsTypeString: string;
309− let returnsTypeString: string;
310309
311310 try {
312311 argsZod = jsonSchemaToZod(combinedInputSchema);
313− argsTypeString = jsonSchemaToTypeString(combinedInputSchema);
314312 } catch {
315313 // Recursive/circular schemas — fall back to z.any()
316314 argsZod = z.any();
317− argsTypeString = "any";
318315 }
319316
320− try {
321− returnsTypeString = op.responseSchema
322− ? jsonSchemaToTypeString(op.responseSchema)
323− : "any";
324− } catch {
325− returnsTypeString = "any";
326− }
317+ // Use openapi-typescript for type strings (handles allOf, oneOf, enums, etc.)
318+ const argsTypeString = jsonSchemaToTypeString(combinedInputSchema);
319+ const returnsTypeString = op.responseSchema
320+ ? jsonSchemaToTypeString(op.responseSchema)
321+ : "any";
327322
328323 const description = op.summary || op.description || `${op.method.toUpperCase()} ${op.path}`;
329324
@@ -332,6 +327,10 @@ export async function generateOpenApiTools( · 2 unmodified lines
332327 approval,
333328 args: argsZod,
334329 returns: z.any(), // OpenAPI responses are complex; use any for runtime
330+ metadata: {
331+ argsType: argsTypeString,
332+ returnsType: returnsTypeString,
333+ },
335334 run: async (input: unknown) => {
336335 const { url, remainingInput } = buildUrl(
337336 baseUrl,
@@ -358,7 +357,7 @@ export async function generateOpenApiTools( · 20 unmodified lines
358357
359358 if (!response.ok) {
360359 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)}`);
362361 }
363362
364363 const contentType = response.headers.get("content-type") ?? "";