mnehmos.trace.mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1662 -0
  3. package/dist/adapters/bootstrap.d.ts +29 -0
  4. package/dist/adapters/bootstrap.d.ts.map +1 -0
  5. package/dist/adapters/bootstrap.js +46 -0
  6. package/dist/adapters/bootstrap.js.map +1 -0
  7. package/dist/adapters/errors.d.ts +94 -0
  8. package/dist/adapters/errors.d.ts.map +1 -0
  9. package/dist/adapters/errors.js +107 -0
  10. package/dist/adapters/errors.js.map +1 -0
  11. package/dist/adapters/graphql/index.d.ts +9 -0
  12. package/dist/adapters/graphql/index.d.ts.map +1 -0
  13. package/dist/adapters/graphql/index.js +9 -0
  14. package/dist/adapters/graphql/index.js.map +1 -0
  15. package/dist/adapters/graphql/sdl-parser.d.ts +74 -0
  16. package/dist/adapters/graphql/sdl-parser.d.ts.map +1 -0
  17. package/dist/adapters/graphql/sdl-parser.js +559 -0
  18. package/dist/adapters/graphql/sdl-parser.js.map +1 -0
  19. package/dist/adapters/grpc/adapter.d.ts +76 -0
  20. package/dist/adapters/grpc/adapter.d.ts.map +1 -0
  21. package/dist/adapters/grpc/adapter.js +362 -0
  22. package/dist/adapters/grpc/adapter.js.map +1 -0
  23. package/dist/adapters/grpc/index.d.ts +10 -0
  24. package/dist/adapters/grpc/index.d.ts.map +1 -0
  25. package/dist/adapters/grpc/index.js +12 -0
  26. package/dist/adapters/grpc/index.js.map +1 -0
  27. package/dist/adapters/grpc/proto-parser.d.ts +76 -0
  28. package/dist/adapters/grpc/proto-parser.d.ts.map +1 -0
  29. package/dist/adapters/grpc/proto-parser.js +523 -0
  30. package/dist/adapters/grpc/proto-parser.js.map +1 -0
  31. package/dist/adapters/grpc/type-converter.d.ts +43 -0
  32. package/dist/adapters/grpc/type-converter.d.ts.map +1 -0
  33. package/dist/adapters/grpc/type-converter.js +270 -0
  34. package/dist/adapters/grpc/type-converter.js.map +1 -0
  35. package/dist/adapters/grpc/types.d.ts +85 -0
  36. package/dist/adapters/grpc/types.d.ts.map +1 -0
  37. package/dist/adapters/grpc/types.js +7 -0
  38. package/dist/adapters/grpc/types.js.map +1 -0
  39. package/dist/adapters/index.d.ts +39 -0
  40. package/dist/adapters/index.d.ts.map +1 -0
  41. package/dist/adapters/index.js +50 -0
  42. package/dist/adapters/index.js.map +1 -0
  43. package/dist/adapters/mcp.d.ts +23 -0
  44. package/dist/adapters/mcp.d.ts.map +1 -0
  45. package/dist/adapters/mcp.js +293 -0
  46. package/dist/adapters/mcp.js.map +1 -0
  47. package/dist/adapters/openapi/adapter.d.ts +213 -0
  48. package/dist/adapters/openapi/adapter.d.ts.map +1 -0
  49. package/dist/adapters/openapi/adapter.js +557 -0
  50. package/dist/adapters/openapi/adapter.js.map +1 -0
  51. package/dist/adapters/openapi/convert.d.ts +120 -0
  52. package/dist/adapters/openapi/convert.d.ts.map +1 -0
  53. package/dist/adapters/openapi/convert.js +363 -0
  54. package/dist/adapters/openapi/convert.js.map +1 -0
  55. package/dist/adapters/openapi/index.d.ts +39 -0
  56. package/dist/adapters/openapi/index.d.ts.map +1 -0
  57. package/dist/adapters/openapi/index.js +48 -0
  58. package/dist/adapters/openapi/index.js.map +1 -0
  59. package/dist/adapters/openapi/parser.d.ts +95 -0
  60. package/dist/adapters/openapi/parser.d.ts.map +1 -0
  61. package/dist/adapters/openapi/parser.js +171 -0
  62. package/dist/adapters/openapi/parser.js.map +1 -0
  63. package/dist/adapters/registry.d.ts +116 -0
  64. package/dist/adapters/registry.d.ts.map +1 -0
  65. package/dist/adapters/registry.js +246 -0
  66. package/dist/adapters/registry.js.map +1 -0
  67. package/dist/adapters/trpc/adapter.d.ts +159 -0
  68. package/dist/adapters/trpc/adapter.d.ts.map +1 -0
  69. package/dist/adapters/trpc/adapter.js +223 -0
  70. package/dist/adapters/trpc/adapter.js.map +1 -0
  71. package/dist/adapters/trpc/extractor.d.ts +218 -0
  72. package/dist/adapters/trpc/extractor.d.ts.map +1 -0
  73. package/dist/adapters/trpc/extractor.js +708 -0
  74. package/dist/adapters/trpc/extractor.js.map +1 -0
  75. package/dist/adapters/trpc/index.d.ts +31 -0
  76. package/dist/adapters/trpc/index.d.ts.map +1 -0
  77. package/dist/adapters/trpc/index.js +40 -0
  78. package/dist/adapters/trpc/index.js.map +1 -0
  79. package/dist/adapters/trpc/parser.d.ts +119 -0
  80. package/dist/adapters/trpc/parser.d.ts.map +1 -0
  81. package/dist/adapters/trpc/parser.js +128 -0
  82. package/dist/adapters/trpc/parser.js.map +1 -0
  83. package/dist/compare/index.d.ts +33 -0
  84. package/dist/compare/index.d.ts.map +1 -0
  85. package/dist/compare/index.js +261 -0
  86. package/dist/compare/index.js.map +1 -0
  87. package/dist/core/types.d.ts +188 -0
  88. package/dist/core/types.d.ts.map +1 -0
  89. package/dist/core/types.js +9 -0
  90. package/dist/core/types.js.map +1 -0
  91. package/dist/extract/index.d.ts +26 -0
  92. package/dist/extract/index.d.ts.map +1 -0
  93. package/dist/extract/index.js +44 -0
  94. package/dist/extract/index.js.map +1 -0
  95. package/dist/index.d.ts +9 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +674 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/languages/base.d.ts +57 -0
  100. package/dist/languages/base.d.ts.map +1 -0
  101. package/dist/languages/base.js +6 -0
  102. package/dist/languages/base.js.map +1 -0
  103. package/dist/languages/bootstrap.d.ts +10 -0
  104. package/dist/languages/bootstrap.d.ts.map +1 -0
  105. package/dist/languages/bootstrap.js +25 -0
  106. package/dist/languages/bootstrap.js.map +1 -0
  107. package/dist/languages/go/handlers/chi.d.ts +24 -0
  108. package/dist/languages/go/handlers/chi.d.ts.map +1 -0
  109. package/dist/languages/go/handlers/chi.js +205 -0
  110. package/dist/languages/go/handlers/chi.js.map +1 -0
  111. package/dist/languages/go/handlers/gin.d.ts +24 -0
  112. package/dist/languages/go/handlers/gin.d.ts.map +1 -0
  113. package/dist/languages/go/handlers/gin.js +156 -0
  114. package/dist/languages/go/handlers/gin.js.map +1 -0
  115. package/dist/languages/go/handlers/stdlib.d.ts +19 -0
  116. package/dist/languages/go/handlers/stdlib.d.ts.map +1 -0
  117. package/dist/languages/go/handlers/stdlib.js +112 -0
  118. package/dist/languages/go/handlers/stdlib.js.map +1 -0
  119. package/dist/languages/go/index.d.ts +18 -0
  120. package/dist/languages/go/index.d.ts.map +1 -0
  121. package/dist/languages/go/index.js +20 -0
  122. package/dist/languages/go/index.js.map +1 -0
  123. package/dist/languages/go/parser.d.ts +33 -0
  124. package/dist/languages/go/parser.d.ts.map +1 -0
  125. package/dist/languages/go/parser.js +95 -0
  126. package/dist/languages/go/parser.js.map +1 -0
  127. package/dist/languages/go/struct-extractor.d.ts +59 -0
  128. package/dist/languages/go/struct-extractor.d.ts.map +1 -0
  129. package/dist/languages/go/struct-extractor.js +483 -0
  130. package/dist/languages/go/struct-extractor.js.map +1 -0
  131. package/dist/languages/go/tag-parser.d.ts +62 -0
  132. package/dist/languages/go/tag-parser.d.ts.map +1 -0
  133. package/dist/languages/go/tag-parser.js +108 -0
  134. package/dist/languages/go/tag-parser.js.map +1 -0
  135. package/dist/languages/go/type-converter.d.ts +32 -0
  136. package/dist/languages/go/type-converter.d.ts.map +1 -0
  137. package/dist/languages/go/type-converter.js +226 -0
  138. package/dist/languages/go/type-converter.js.map +1 -0
  139. package/dist/languages/go/types.d.ts +153 -0
  140. package/dist/languages/go/types.d.ts.map +1 -0
  141. package/dist/languages/go/types.js +6 -0
  142. package/dist/languages/go/types.js.map +1 -0
  143. package/dist/languages/import-resolver.d.ts +645 -0
  144. package/dist/languages/import-resolver.d.ts.map +1 -0
  145. package/dist/languages/import-resolver.js +1278 -0
  146. package/dist/languages/import-resolver.js.map +1 -0
  147. package/dist/languages/index.d.ts +34 -0
  148. package/dist/languages/index.d.ts.map +1 -0
  149. package/dist/languages/index.js +93 -0
  150. package/dist/languages/index.js.map +1 -0
  151. package/dist/languages/json-schema.d.ts +40 -0
  152. package/dist/languages/json-schema.d.ts.map +1 -0
  153. package/dist/languages/json-schema.js +188 -0
  154. package/dist/languages/json-schema.js.map +1 -0
  155. package/dist/languages/python-ast/index.d.ts +8 -0
  156. package/dist/languages/python-ast/index.d.ts.map +1 -0
  157. package/dist/languages/python-ast/index.js +7 -0
  158. package/dist/languages/python-ast/index.js.map +1 -0
  159. package/dist/languages/python-ast/parser.d.ts +174 -0
  160. package/dist/languages/python-ast/parser.d.ts.map +1 -0
  161. package/dist/languages/python-ast/parser.js +1205 -0
  162. package/dist/languages/python-ast/parser.js.map +1 -0
  163. package/dist/languages/python-ast/type-resolver.d.ts +75 -0
  164. package/dist/languages/python-ast/type-resolver.d.ts.map +1 -0
  165. package/dist/languages/python-ast/type-resolver.js +421 -0
  166. package/dist/languages/python-ast/type-resolver.js.map +1 -0
  167. package/dist/languages/python-ast/types.d.ts +216 -0
  168. package/dist/languages/python-ast/types.d.ts.map +1 -0
  169. package/dist/languages/python-ast/types.js +6 -0
  170. package/dist/languages/python-ast/types.js.map +1 -0
  171. package/dist/languages/python.d.ts +55 -0
  172. package/dist/languages/python.d.ts.map +1 -0
  173. package/dist/languages/python.js +311 -0
  174. package/dist/languages/python.js.map +1 -0
  175. package/dist/languages/typescript.d.ts +272 -0
  176. package/dist/languages/typescript.d.ts.map +1 -0
  177. package/dist/languages/typescript.js +1381 -0
  178. package/dist/languages/typescript.js.map +1 -0
  179. package/dist/patterns/base.d.ts +146 -0
  180. package/dist/patterns/base.d.ts.map +1 -0
  181. package/dist/patterns/base.js +89 -0
  182. package/dist/patterns/base.js.map +1 -0
  183. package/dist/patterns/errors.d.ts +172 -0
  184. package/dist/patterns/errors.d.ts.map +1 -0
  185. package/dist/patterns/errors.js +185 -0
  186. package/dist/patterns/errors.js.map +1 -0
  187. package/dist/patterns/extractors.d.ts +170 -0
  188. package/dist/patterns/extractors.d.ts.map +1 -0
  189. package/dist/patterns/extractors.js +305 -0
  190. package/dist/patterns/extractors.js.map +1 -0
  191. package/dist/patterns/graphql/apollo-client.d.ts +80 -0
  192. package/dist/patterns/graphql/apollo-client.d.ts.map +1 -0
  193. package/dist/patterns/graphql/apollo-client.js +800 -0
  194. package/dist/patterns/graphql/apollo-client.js.map +1 -0
  195. package/dist/patterns/graphql/apollo-server.d.ts +55 -0
  196. package/dist/patterns/graphql/apollo-server.d.ts.map +1 -0
  197. package/dist/patterns/graphql/apollo-server.js +523 -0
  198. package/dist/patterns/graphql/apollo-server.js.map +1 -0
  199. package/dist/patterns/graphql/index.d.ts +11 -0
  200. package/dist/patterns/graphql/index.d.ts.map +1 -0
  201. package/dist/patterns/graphql/index.js +12 -0
  202. package/dist/patterns/graphql/index.js.map +1 -0
  203. package/dist/patterns/graphql/types.d.ts +213 -0
  204. package/dist/patterns/graphql/types.d.ts.map +1 -0
  205. package/dist/patterns/graphql/types.js +16 -0
  206. package/dist/patterns/graphql/types.js.map +1 -0
  207. package/dist/patterns/http-clients/axios.d.ts +148 -0
  208. package/dist/patterns/http-clients/axios.d.ts.map +1 -0
  209. package/dist/patterns/http-clients/axios.js +652 -0
  210. package/dist/patterns/http-clients/axios.js.map +1 -0
  211. package/dist/patterns/http-clients/fetch.d.ts +88 -0
  212. package/dist/patterns/http-clients/fetch.d.ts.map +1 -0
  213. package/dist/patterns/http-clients/fetch.js +364 -0
  214. package/dist/patterns/http-clients/fetch.js.map +1 -0
  215. package/dist/patterns/http-clients/index.d.ts +36 -0
  216. package/dist/patterns/http-clients/index.d.ts.map +1 -0
  217. package/dist/patterns/http-clients/index.js +50 -0
  218. package/dist/patterns/http-clients/index.js.map +1 -0
  219. package/dist/patterns/http-clients/property-access.d.ts +46 -0
  220. package/dist/patterns/http-clients/property-access.d.ts.map +1 -0
  221. package/dist/patterns/http-clients/property-access.js +818 -0
  222. package/dist/patterns/http-clients/property-access.js.map +1 -0
  223. package/dist/patterns/http-clients/type-inference.d.ts +48 -0
  224. package/dist/patterns/http-clients/type-inference.d.ts.map +1 -0
  225. package/dist/patterns/http-clients/type-inference.js +293 -0
  226. package/dist/patterns/http-clients/type-inference.js.map +1 -0
  227. package/dist/patterns/http-clients/types.d.ts +168 -0
  228. package/dist/patterns/http-clients/types.d.ts.map +1 -0
  229. package/dist/patterns/http-clients/types.js +10 -0
  230. package/dist/patterns/http-clients/types.js.map +1 -0
  231. package/dist/patterns/http-clients/url-extractor.d.ts +53 -0
  232. package/dist/patterns/http-clients/url-extractor.d.ts.map +1 -0
  233. package/dist/patterns/http-clients/url-extractor.js +338 -0
  234. package/dist/patterns/http-clients/url-extractor.js.map +1 -0
  235. package/dist/patterns/index.d.ts +44 -0
  236. package/dist/patterns/index.d.ts.map +1 -0
  237. package/dist/patterns/index.js +49 -0
  238. package/dist/patterns/index.js.map +1 -0
  239. package/dist/patterns/python/aiohttp.d.ts +21 -0
  240. package/dist/patterns/python/aiohttp.d.ts.map +1 -0
  241. package/dist/patterns/python/aiohttp.js +188 -0
  242. package/dist/patterns/python/aiohttp.js.map +1 -0
  243. package/dist/patterns/python/httpx.d.ts +20 -0
  244. package/dist/patterns/python/httpx.d.ts.map +1 -0
  245. package/dist/patterns/python/httpx.js +183 -0
  246. package/dist/patterns/python/httpx.js.map +1 -0
  247. package/dist/patterns/python/index.d.ts +32 -0
  248. package/dist/patterns/python/index.d.ts.map +1 -0
  249. package/dist/patterns/python/index.js +63 -0
  250. package/dist/patterns/python/index.js.map +1 -0
  251. package/dist/patterns/python/property-access.d.ts +27 -0
  252. package/dist/patterns/python/property-access.d.ts.map +1 -0
  253. package/dist/patterns/python/property-access.js +132 -0
  254. package/dist/patterns/python/property-access.js.map +1 -0
  255. package/dist/patterns/python/requests.d.ts +19 -0
  256. package/dist/patterns/python/requests.d.ts.map +1 -0
  257. package/dist/patterns/python/requests.js +239 -0
  258. package/dist/patterns/python/requests.js.map +1 -0
  259. package/dist/patterns/python/types.d.ts +95 -0
  260. package/dist/patterns/python/types.d.ts.map +1 -0
  261. package/dist/patterns/python/types.js +43 -0
  262. package/dist/patterns/python/types.js.map +1 -0
  263. package/dist/patterns/registry.d.ts +181 -0
  264. package/dist/patterns/registry.d.ts.map +1 -0
  265. package/dist/patterns/registry.js +304 -0
  266. package/dist/patterns/registry.js.map +1 -0
  267. package/dist/patterns/rest/express.d.ts +78 -0
  268. package/dist/patterns/rest/express.d.ts.map +1 -0
  269. package/dist/patterns/rest/express.js +289 -0
  270. package/dist/patterns/rest/express.js.map +1 -0
  271. package/dist/patterns/rest/fastify.d.ts +93 -0
  272. package/dist/patterns/rest/fastify.d.ts.map +1 -0
  273. package/dist/patterns/rest/fastify.js +420 -0
  274. package/dist/patterns/rest/fastify.js.map +1 -0
  275. package/dist/patterns/rest/index.d.ts +31 -0
  276. package/dist/patterns/rest/index.d.ts.map +1 -0
  277. package/dist/patterns/rest/index.js +45 -0
  278. package/dist/patterns/rest/index.js.map +1 -0
  279. package/dist/patterns/rest/middleware.d.ts +25 -0
  280. package/dist/patterns/rest/middleware.d.ts.map +1 -0
  281. package/dist/patterns/rest/middleware.js +219 -0
  282. package/dist/patterns/rest/middleware.js.map +1 -0
  283. package/dist/patterns/rest/path-parser.d.ts +50 -0
  284. package/dist/patterns/rest/path-parser.d.ts.map +1 -0
  285. package/dist/patterns/rest/path-parser.js +137 -0
  286. package/dist/patterns/rest/path-parser.js.map +1 -0
  287. package/dist/patterns/rest/response-inference.d.ts +44 -0
  288. package/dist/patterns/rest/response-inference.d.ts.map +1 -0
  289. package/dist/patterns/rest/response-inference.js +218 -0
  290. package/dist/patterns/rest/response-inference.js.map +1 -0
  291. package/dist/patterns/rest/types.d.ts +102 -0
  292. package/dist/patterns/rest/types.d.ts.map +1 -0
  293. package/dist/patterns/rest/types.js +10 -0
  294. package/dist/patterns/rest/types.js.map +1 -0
  295. package/dist/patterns/types.d.ts +105 -0
  296. package/dist/patterns/types.d.ts.map +1 -0
  297. package/dist/patterns/types.js +11 -0
  298. package/dist/patterns/types.js.map +1 -0
  299. package/dist/report/index.d.ts +11 -0
  300. package/dist/report/index.d.ts.map +1 -0
  301. package/dist/report/index.js +55 -0
  302. package/dist/report/index.js.map +1 -0
  303. package/dist/tools/contract-comments.d.ts +48 -0
  304. package/dist/tools/contract-comments.d.ts.map +1 -0
  305. package/dist/tools/contract-comments.js +130 -0
  306. package/dist/tools/contract-comments.js.map +1 -0
  307. package/dist/tools/index.d.ts +6 -0
  308. package/dist/tools/index.d.ts.map +1 -0
  309. package/dist/tools/index.js +6 -0
  310. package/dist/tools/index.js.map +1 -0
  311. package/dist/tools/scaffold.d.ts +38 -0
  312. package/dist/tools/scaffold.d.ts.map +1 -0
  313. package/dist/tools/scaffold.js +373 -0
  314. package/dist/tools/scaffold.js.map +1 -0
  315. package/dist/trace/index.d.ts +28 -0
  316. package/dist/trace/index.d.ts.map +1 -0
  317. package/dist/trace/index.js +45 -0
  318. package/dist/trace/index.js.map +1 -0
  319. package/dist/types.d.ts +135 -0
  320. package/dist/types.d.ts.map +1 -0
  321. package/dist/types.js +22 -0
  322. package/dist/types.js.map +1 -0
  323. package/dist/watch/cache.d.ts +41 -0
  324. package/dist/watch/cache.d.ts.map +1 -0
  325. package/dist/watch/cache.js +230 -0
  326. package/dist/watch/cache.js.map +1 -0
  327. package/dist/watch/index.d.ts +9 -0
  328. package/dist/watch/index.d.ts.map +1 -0
  329. package/dist/watch/index.js +7 -0
  330. package/dist/watch/index.js.map +1 -0
  331. package/dist/watch/project.d.ts +128 -0
  332. package/dist/watch/project.d.ts.map +1 -0
  333. package/dist/watch/project.js +152 -0
  334. package/dist/watch/project.js.map +1 -0
  335. package/dist/watch/watcher.d.ts +76 -0
  336. package/dist/watch/watcher.d.ts.map +1 -0
  337. package/dist/watch/watcher.js +235 -0
  338. package/dist/watch/watcher.js.map +1 -0
  339. package/package.json +70 -0
@@ -0,0 +1,1205 @@
1
+ /**
2
+ * Python AST Parser
3
+ * Parses Python source code to extract endpoints, tools, and models
4
+ *
5
+ * Uses regex-based parsing as a fallback when tree-sitter is not available.
6
+ * The parser detects:
7
+ * - FastAPI endpoints (@app.get, @app.post, @router.get, etc.)
8
+ * - Flask routes (@app.route, @blueprint.route)
9
+ * - MCP tools (@mcp.tool, @server.tool)
10
+ * - Pydantic BaseModel classes
11
+ * - Typed functions (for type annotation testing)
12
+ */
13
+ import { TypeResolver } from './type-resolver.js';
14
+ import { detectAllHttpCalls } from '../../patterns/python/index.js';
15
+ /**
16
+ * Python AST Parser implementation
17
+ */
18
+ export class PythonASTParser {
19
+ name = 'python';
20
+ filePatterns = ['**/*.py', '**/*.pyi'];
21
+ typeResolver;
22
+ routers = new Map();
23
+ blueprints = new Map();
24
+ servers = new Map(); // variable name -> server name
25
+ enums = new Map();
26
+ initialized = false;
27
+ constructor() {
28
+ this.typeResolver = new TypeResolver();
29
+ }
30
+ /**
31
+ * Initialize the parser
32
+ */
33
+ async initialize() {
34
+ this.initialized = true;
35
+ return true;
36
+ }
37
+ /**
38
+ * Extract producer schemas from Python source
39
+ */
40
+ async extractSchemas(options) {
41
+ const content = options.content;
42
+ if (!content) {
43
+ return [];
44
+ }
45
+ // Reset state for new parsing
46
+ this.routers.clear();
47
+ this.blueprints.clear();
48
+ this.servers.clear();
49
+ this.enums.clear();
50
+ this.typeResolver = new TypeResolver();
51
+ const schemas = [];
52
+ const lines = content.split('\n');
53
+ try {
54
+ // First pass: collect routers, blueprints, servers, enums, and models
55
+ this.collectDefinitions(content, lines);
56
+ // Second pass: extract endpoints and tools
57
+ const endpoints = this.extractEndpoints(content, lines);
58
+ schemas.push(...endpoints);
59
+ // Extract Pydantic models
60
+ const models = this.extractPydanticModels(content, lines);
61
+ schemas.push(...models);
62
+ // Extract typed functions (for type annotation testing)
63
+ // These are functions with type annotations but without decorators
64
+ const typedFunctions = this.extractTypedFunctions(content, lines);
65
+ schemas.push(...typedFunctions);
66
+ }
67
+ catch (error) {
68
+ // Silently handle parse errors - return what we have
69
+ if (process.env.DEBUG_TRACE_MCP) {
70
+ console.error('[PythonASTParser] Parse error:', error);
71
+ }
72
+ }
73
+ return schemas;
74
+ }
75
+ /**
76
+ * Trace consumer usage - detects HTTP client calls (requests, httpx, aiohttp)
77
+ *
78
+ * @param options - TraceOptions containing content to analyze
79
+ * @returns ConsumerSchema[] representing HTTP client calls found
80
+ */
81
+ async traceUsage(options) {
82
+ const content = options.content;
83
+ if (!content) {
84
+ return [];
85
+ }
86
+ // Detect HTTP client calls from all supported libraries
87
+ const httpCalls = detectAllHttpCalls(content);
88
+ // Convert HTTP calls to ConsumerSchema format
89
+ return httpCalls.map(call => this.httpCallToConsumerSchema(call));
90
+ }
91
+ /**
92
+ * Convert a Python HTTP call to ConsumerSchema format
93
+ */
94
+ httpCallToConsumerSchema(call) {
95
+ // Build toolName as "METHOD URL" format
96
+ const url = call.url || '<dynamic>';
97
+ const toolName = `${call.method} ${url}`;
98
+ // Build arguments provided
99
+ const argumentsProvided = {
100
+ library: call.library,
101
+ method: call.method,
102
+ url: call.url,
103
+ isAsync: call.isAsync
104
+ };
105
+ // Add session/client flags
106
+ if (call.isSession) {
107
+ argumentsProvided.isSession = true;
108
+ }
109
+ if (call.isClient) {
110
+ argumentsProvided.isClient = true;
111
+ }
112
+ if (call.isAsyncClient) {
113
+ argumentsProvided.isAsyncClient = true;
114
+ }
115
+ // Add URL-related flags
116
+ if (call.isDynamicUrl) {
117
+ argumentsProvided.isDynamicUrl = true;
118
+ }
119
+ if (call.hasQueryParams) {
120
+ argumentsProvided.hasQueryParams = true;
121
+ }
122
+ // Add additional info if available
123
+ if (call.hasHeaders) {
124
+ argumentsProvided.hasHeaders = true;
125
+ }
126
+ if (call.hasBody) {
127
+ argumentsProvided.hasBody = true;
128
+ }
129
+ if (call.responseVariable) {
130
+ argumentsProvided.responseVariable = call.responseVariable;
131
+ }
132
+ if (call.pathParams && call.pathParams.length > 0) {
133
+ argumentsProvided.pathParams = call.pathParams;
134
+ }
135
+ // Build expected properties from response property access
136
+ const expectedProperties = call.responseProperties || [];
137
+ return {
138
+ toolName,
139
+ argumentsProvided,
140
+ expectedProperties,
141
+ callSite: {
142
+ file: '',
143
+ line: call.line,
144
+ column: 0
145
+ }
146
+ };
147
+ }
148
+ /**
149
+ * Collect router, blueprint, server, enum, and model definitions
150
+ */
151
+ collectDefinitions(content, lines) {
152
+ // Find APIRouter definitions: router = APIRouter(prefix="/api/v1")
153
+ const routerPattern = /(\w+)\s*=\s*APIRouter\s*\(([^)]*)\)/g;
154
+ let match;
155
+ while ((match = routerPattern.exec(content)) !== null) {
156
+ const varName = match[1];
157
+ const args = match[2];
158
+ const prefixMatch = args.match(/prefix\s*=\s*["']([^"']+)["']/);
159
+ const prefix = prefixMatch ? prefixMatch[1] : '';
160
+ this.routers.set(varName, { variableName: varName, prefix });
161
+ }
162
+ // Find Blueprint definitions: bp = Blueprint("name", __name__, url_prefix="/api")
163
+ const blueprintPattern = /(\w+)\s*=\s*Blueprint\s*\(\s*["'][^"']+["']\s*,\s*[^,]+(?:,\s*url_prefix\s*=\s*["']([^"']+)["'])?\s*\)/g;
164
+ while ((match = blueprintPattern.exec(content)) !== null) {
165
+ const varName = match[1];
166
+ const prefix = match[2] || '';
167
+ this.blueprints.set(varName, { variableName: varName, prefix });
168
+ }
169
+ // Find Server definitions: mcp = Server("name") or server = Server("name")
170
+ const serverPattern = /(\w+)\s*=\s*Server\s*\(\s*["']([^"']+)["']\s*\)/g;
171
+ while ((match = serverPattern.exec(content)) !== null) {
172
+ const varName = match[1];
173
+ const serverName = match[2];
174
+ this.servers.set(varName, serverName);
175
+ }
176
+ // Find include_router patterns: router.include_router(sub_router)
177
+ const includePattern = /(\w+)\.include_router\s*\(\s*(\w+)\s*\)/g;
178
+ while ((match = includePattern.exec(content)) !== null) {
179
+ const parentRouter = match[1];
180
+ const childRouter = match[2];
181
+ const parentDef = this.routers.get(parentRouter);
182
+ const childDef = this.routers.get(childRouter);
183
+ if (parentDef && childDef) {
184
+ // Update child router's prefix to include parent's prefix
185
+ childDef.prefix = parentDef.prefix + childDef.prefix;
186
+ }
187
+ }
188
+ // Find Enum definitions
189
+ this.extractEnums(content, lines);
190
+ // Pre-register Pydantic models for type resolution
191
+ this.preRegisterModels(content, lines);
192
+ }
193
+ /**
194
+ * Extract enum definitions
195
+ */
196
+ extractEnums(content, _lines) {
197
+ // Match class Name(Enum): or class Name(IntEnum): or class Name(str, Enum):
198
+ // Use non-greedy [^)]*? so (?:Enum|IntEnum) can actually match
199
+ // Use \r?\n to handle both Unix and Windows line endings
200
+ // Use [ \t]+ instead of \s+ to avoid matching across blank lines
201
+ const enumPattern = /class\s+(\w+)\s*\([^)]*?(?:Enum|IntEnum)[^)]*\)\s*:((?:\r?\n[ \t]+.+)+)/g;
202
+ let match;
203
+ while ((match = enumPattern.exec(content)) !== null) {
204
+ const enumName = match[1];
205
+ const body = match[2];
206
+ const isIntEnum = match[0].includes('IntEnum');
207
+ const values = [];
208
+ // Pattern for enum values: NAME = "value" or NAME = 123
209
+ // Capture quoted strings or integers, skip method definitions
210
+ const valuePattern = /^\s+([A-Z_][A-Z0-9_]*)\s*=\s*(?:["']([^"']+)["']|(\d+))/gm;
211
+ let valueMatch;
212
+ while ((valueMatch = valuePattern.exec(body)) !== null) {
213
+ const name = valueMatch[1];
214
+ // Parse value - use captured string or integer
215
+ let value;
216
+ if (valueMatch[3] !== undefined) {
217
+ // Integer value
218
+ value = parseInt(valueMatch[3], 10);
219
+ }
220
+ else if (valueMatch[2] !== undefined) {
221
+ // String value
222
+ value = valueMatch[2];
223
+ }
224
+ else {
225
+ continue; // Skip if neither matched
226
+ }
227
+ values.push({ name, value });
228
+ }
229
+ if (values.length > 0) {
230
+ this.enums.set(enumName, { name: enumName, values, isIntEnum });
231
+ this.typeResolver.registerEnum({ name: enumName, values, isIntEnum });
232
+ }
233
+ }
234
+ }
235
+ /**
236
+ * Pre-register Pydantic models for type resolution
237
+ */
238
+ preRegisterModels(content, lines) {
239
+ const modelPattern = /class\s+(\w+)\s*\(\s*(?:.*?BaseModel.*?|(\w+))\s*\)\s*:/g;
240
+ let match;
241
+ while ((match = modelPattern.exec(content)) !== null) {
242
+ const className = match[1];
243
+ const lineNum = this.getLineNumber(content, match.index);
244
+ // Check if it extends BaseModel (directly or indirectly)
245
+ const bases = this.extractBases(match[0]);
246
+ // Simple check - register all classes for now, we'll filter later
247
+ this.typeResolver.registerModel({
248
+ name: className,
249
+ fields: [],
250
+ bases,
251
+ location: { file: '', line: lineNum, column: 0 }
252
+ });
253
+ }
254
+ }
255
+ /**
256
+ * Extract base classes from class definition
257
+ */
258
+ extractBases(classLine) {
259
+ const match = classLine.match(/class\s+\w+\s*\(([^)]+)\)/);
260
+ if (!match)
261
+ return [];
262
+ return match[1].split(',').map(b => b.trim()).filter(b => b && !b.includes('='));
263
+ }
264
+ /**
265
+ * Extract FastAPI and Flask endpoints
266
+ */
267
+ extractEndpoints(content, lines) {
268
+ const schemas = [];
269
+ // Pattern for decorated functions
270
+ // Matches: @something.method("/path") or @something.method("/path", ...)
271
+ // followed by async def or def
272
+ const decoratedFuncPattern = /@(\w+)\.(\w+)\s*\(([^)]*)\)[\s\S]*?(?=@\w+\.|class\s|def\s|async\s+def\s)/g;
273
+ // More specific approach: find all decorators and their associated functions
274
+ const funcBlocks = this.extractDecoratedFunctions(content, lines);
275
+ for (const block of funcBlocks) {
276
+ const endpoints = this.processDecoratedFunction(block, lines);
277
+ if (endpoints) {
278
+ // Handle both single schema and array of schemas (for multi-method Flask routes)
279
+ if (Array.isArray(endpoints)) {
280
+ schemas.push(...endpoints);
281
+ }
282
+ else {
283
+ schemas.push(endpoints);
284
+ }
285
+ }
286
+ }
287
+ return schemas;
288
+ }
289
+ /**
290
+ * Extract all decorated function blocks
291
+ */
292
+ extractDecoratedFunctions(content, lines) {
293
+ const blocks = [];
294
+ let i = 0;
295
+ while (i < lines.length) {
296
+ const line = lines[i];
297
+ // Check if this line starts a decorator
298
+ if (line.trim().startsWith('@')) {
299
+ const decorators = [];
300
+ const startLine = i + 1;
301
+ // Collect all decorators
302
+ while (i < lines.length && lines[i].trim().startsWith('@')) {
303
+ // Handle multi-line decorators
304
+ let decorator = lines[i].trim();
305
+ while (!decorator.includes(')') && !decorator.endsWith(':')) {
306
+ i++;
307
+ if (i < lines.length) {
308
+ decorator += ' ' + lines[i].trim();
309
+ }
310
+ }
311
+ decorators.push(decorator);
312
+ i++;
313
+ }
314
+ // Now we should be at the function definition
315
+ if (i < lines.length) {
316
+ const funcLine = lines[i];
317
+ if (funcLine.trim().startsWith('def ') || funcLine.trim().startsWith('async def ')) {
318
+ // Collect function definition (may span multiple lines)
319
+ let funcDef = funcLine;
320
+ while (!funcDef.includes(':') || (funcDef.split('(').length > funcDef.split(')').length)) {
321
+ i++;
322
+ if (i < lines.length) {
323
+ funcDef += '\n' + lines[i];
324
+ }
325
+ }
326
+ // Get the function body (for docstring)
327
+ const bodyStart = i + 1;
328
+ let bodyEnd = bodyStart;
329
+ const baseIndent = this.getIndent(funcLine);
330
+ while (bodyEnd < lines.length) {
331
+ const bodyLine = lines[bodyEnd];
332
+ // Empty lines are ok
333
+ if (bodyLine.trim() === '') {
334
+ bodyEnd++;
335
+ continue;
336
+ }
337
+ // Check indentation
338
+ const lineIndent = this.getIndent(bodyLine);
339
+ if (lineIndent <= baseIndent && bodyLine.trim() !== '') {
340
+ break;
341
+ }
342
+ bodyEnd++;
343
+ }
344
+ // Capture more lines for detailed docstrings (up to 30 lines)
345
+ const funcBody = lines.slice(bodyStart, Math.min(bodyStart + 30, bodyEnd)).join('\n');
346
+ blocks.push({
347
+ decorators,
348
+ funcDef,
349
+ funcBody,
350
+ startLine,
351
+ endLine: bodyEnd
352
+ });
353
+ }
354
+ }
355
+ }
356
+ i++;
357
+ }
358
+ return blocks;
359
+ }
360
+ /**
361
+ * Get indentation level of a line
362
+ */
363
+ getIndent(line) {
364
+ const match = line.match(/^(\s*)/);
365
+ return match ? match[1].length : 0;
366
+ }
367
+ /**
368
+ * Process a decorated function block into a schema (or array of schemas for multi-method routes)
369
+ */
370
+ processDecoratedFunction(block, _lines) {
371
+ // Parse decorators
372
+ for (const decorator of block.decorators) {
373
+ // FastAPI pattern: @app.get("/path") or @router.post("/path")
374
+ const fastapiMatch = decorator.match(/@(\w+)\.(get|post|put|patch|delete|options|head)\s*\(([^)]*)\)/i);
375
+ if (fastapiMatch) {
376
+ return this.processFastAPIEndpoint(fastapiMatch, block, decorator);
377
+ }
378
+ // Flask pattern: @app.route("/path") or @bp.route("/path", methods=["GET"])
379
+ const flaskMatch = decorator.match(/@(\w+)\.route\s*\(([^)]*)\)/);
380
+ if (flaskMatch) {
381
+ return this.processFlaskRoute(flaskMatch, block, decorator);
382
+ }
383
+ // MCP pattern: @mcp.tool() or @server.tool()
384
+ const mcpMatch = decorator.match(/@(\w+)\.tool\s*\(\s*\)/);
385
+ if (mcpMatch) {
386
+ return this.processMCPTool(mcpMatch, block);
387
+ }
388
+ }
389
+ return null;
390
+ }
391
+ /**
392
+ * Process FastAPI endpoint
393
+ */
394
+ processFastAPIEndpoint(match, block, decorator) {
395
+ const routerVar = match[1];
396
+ const method = match[2].toUpperCase();
397
+ const args = match[3];
398
+ // Extract path from first argument - handle empty string paths
399
+ const pathMatch = args.match(/["']([^"']*)["']/);
400
+ let path = pathMatch !== null ? pathMatch[1] : '/';
401
+ // Add router prefix if applicable
402
+ const routerDef = this.routers.get(routerVar);
403
+ if (routerDef && routerDef.prefix) {
404
+ // Normalize prefix first
405
+ let prefix = routerDef.prefix.replace(/\/+$/, '');
406
+ // When path is empty, just use the prefix
407
+ if (path === '' || path === '/') {
408
+ path = prefix;
409
+ }
410
+ else {
411
+ // Ensure path starts with / if not empty
412
+ if (!path.startsWith('/')) {
413
+ path = '/' + path;
414
+ }
415
+ path = prefix + path;
416
+ }
417
+ }
418
+ // Normalize path - remove trailing slashes aggressively
419
+ path = path.trim().replace(/[\r\n\t]/g, '');
420
+ if (!path.startsWith('/'))
421
+ path = '/' + path;
422
+ // Remove ALL trailing slashes
423
+ path = path.replace(/\/+$/g, '') || '/';
424
+ // Parse function definition
425
+ const funcInfo = this.parseFunctionDef(block.funcDef);
426
+ // Extract status_code from decorator
427
+ const statusMatch = decorator.match(/status_code\s*=\s*(\d+)/);
428
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
429
+ // Extract response_model
430
+ const responseModelMatch = decorator.match(/response_model\s*=\s*(\w+)/);
431
+ const responseModel = responseModelMatch ? responseModelMatch[1] : undefined;
432
+ // Extract parameters
433
+ const parameters = this.extractParameters(funcInfo.params, path);
434
+ // Build input schema
435
+ const inputSchema = this.buildInputSchema(parameters);
436
+ // Build output schema
437
+ const outputSchema = responseModel
438
+ ? this.typeResolver.resolve(responseModel)
439
+ : funcInfo.returnType
440
+ ? this.typeResolver.resolve(funcInfo.returnType)
441
+ : {};
442
+ // Extract docstring
443
+ const docstring = this.extractDocstring(block.funcBody);
444
+ // Final path normalization to ensure no trailing slashes
445
+ // Apply double normalization for extra safety
446
+ let normalizedPath = this.normalizePath(path);
447
+ // One more explicit check
448
+ if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
449
+ normalizedPath = normalizedPath.slice(0, -1);
450
+ }
451
+ // Final aggressive trailing slash removal
452
+ const finalPath = normalizedPath.replace(/\/+$/, '') || '/';
453
+ return {
454
+ id: funcInfo.name,
455
+ toolName: funcInfo.name,
456
+ method,
457
+ path: finalPath,
458
+ async: funcInfo.isAsync,
459
+ statusCode,
460
+ type: 'endpoint',
461
+ inputSchema,
462
+ outputSchema,
463
+ description: docstring,
464
+ location: { file: '', line: block.startLine, column: 0 }
465
+ };
466
+ }
467
+ /**
468
+ * Process Flask route - returns array of schemas for multi-method routes
469
+ */
470
+ processFlaskRoute(match, block, decorator) {
471
+ const routerVar = match[1];
472
+ const args = match[2];
473
+ // Extract path
474
+ const pathMatch = args.match(/["']([^"']+)["']/);
475
+ let path = pathMatch ? pathMatch[1] : '/';
476
+ // Add blueprint prefix if applicable
477
+ const blueprintDef = this.blueprints.get(routerVar);
478
+ if (blueprintDef) {
479
+ path = blueprintDef.prefix + path;
480
+ }
481
+ // Convert Flask path parameters to OpenAPI style
482
+ // <int:user_id> -> {user_id}
483
+ const pathParams = this.extractFlaskPathParams(path);
484
+ path = path.replace(/<(\w+:)?(\w+)>/g, '{$2}');
485
+ // Extract methods
486
+ const methodsMatch = args.match(/methods\s*=\s*\[([^\]]+)\]/);
487
+ let methods = ['GET'];
488
+ if (methodsMatch) {
489
+ methods = methodsMatch[1]
490
+ .split(',')
491
+ .map(m => m.trim().replace(/["']/g, '').toUpperCase());
492
+ }
493
+ // Parse function definition
494
+ const funcInfo = this.parseFunctionDef(block.funcDef);
495
+ // Extract parameters
496
+ const parameters = this.extractParameters(funcInfo.params, path, pathParams);
497
+ // Build input schema
498
+ const inputSchema = this.buildInputSchema(parameters);
499
+ // Extract docstring
500
+ const docstring = this.extractDocstring(block.funcBody);
501
+ // For multiple methods, create separate schemas for each
502
+ if (methods.length > 1) {
503
+ return methods.map(method => ({
504
+ id: `${funcInfo.name}_${method}`,
505
+ toolName: funcInfo.name,
506
+ method: method,
507
+ path,
508
+ async: funcInfo.isAsync,
509
+ type: 'endpoint',
510
+ inputSchema,
511
+ outputSchema: {},
512
+ description: docstring,
513
+ location: { file: '', line: block.startLine, column: 0 }
514
+ }));
515
+ }
516
+ // Single method
517
+ const method = methods[0];
518
+ return {
519
+ id: `${funcInfo.name}_${method}`,
520
+ toolName: funcInfo.name,
521
+ method,
522
+ path,
523
+ async: funcInfo.isAsync,
524
+ type: 'endpoint',
525
+ inputSchema,
526
+ outputSchema: {},
527
+ description: docstring,
528
+ location: { file: '', line: block.startLine, column: 0 }
529
+ };
530
+ }
531
+ /**
532
+ * Extract Flask path parameters with their types
533
+ */
534
+ extractFlaskPathParams(path) {
535
+ const params = new Map();
536
+ const pattern = /<(\w+):(\w+)>/g;
537
+ let match;
538
+ while ((match = pattern.exec(path)) !== null) {
539
+ const converter = match[1];
540
+ const paramName = match[2];
541
+ params.set(paramName, converter);
542
+ }
543
+ // Also match simple <param> without converter
544
+ const simplePattern = /<(\w+)>/g;
545
+ while ((match = simplePattern.exec(path)) !== null) {
546
+ if (!match[0].includes(':')) {
547
+ params.set(match[1], 'string');
548
+ }
549
+ }
550
+ return params;
551
+ }
552
+ /**
553
+ * Process MCP tool
554
+ */
555
+ processMCPTool(match, block) {
556
+ const serverVar = match[1];
557
+ const serverName = this.servers.get(serverVar);
558
+ // Parse function definition
559
+ const funcInfo = this.parseFunctionDef(block.funcDef);
560
+ // Extract parameters
561
+ const parameters = this.extractParameters(funcInfo.params, '');
562
+ // Build input schema
563
+ const inputSchema = this.buildInputSchema(parameters);
564
+ // Build output schema
565
+ const outputSchema = funcInfo.returnType
566
+ ? this.typeResolver.resolve(funcInfo.returnType)
567
+ : {};
568
+ // Extract docstring
569
+ const docstring = this.extractDocstring(block.funcBody);
570
+ // Extract parameter descriptions from docstring
571
+ const paramDescriptions = this.extractParamDescriptions(block.funcBody);
572
+ for (const [paramName, description] of paramDescriptions) {
573
+ if (inputSchema.properties?.[paramName]) {
574
+ inputSchema.properties[paramName].description = description;
575
+ }
576
+ }
577
+ return {
578
+ id: funcInfo.name,
579
+ toolName: funcInfo.name,
580
+ async: funcInfo.isAsync,
581
+ type: 'tool',
582
+ inputSchema,
583
+ outputSchema,
584
+ description: docstring,
585
+ location: { file: '', line: block.startLine, column: 0 }
586
+ };
587
+ }
588
+ /**
589
+ * Parse function definition
590
+ */
591
+ parseFunctionDef(funcDef) {
592
+ const isAsync = funcDef.trim().startsWith('async ');
593
+ // Extract function name
594
+ const nameMatch = funcDef.match(/def\s+(\w+)\s*\(/);
595
+ const name = nameMatch ? nameMatch[1] : 'unknown';
596
+ // Extract parameters - handle nested parentheses
597
+ const paramsStr = this.extractParamsFromFuncDef(funcDef);
598
+ const params = this.parseParams(paramsStr);
599
+ // Extract return type
600
+ const returnMatch = funcDef.match(/->\s*([^:]+):/);
601
+ const returnType = returnMatch ? returnMatch[1].trim() : undefined;
602
+ return { name, isAsync, params, returnType };
603
+ }
604
+ /**
605
+ * Extract parameters from function definition, handling nested parentheses
606
+ */
607
+ extractParamsFromFuncDef(funcDef) {
608
+ const startIdx = funcDef.indexOf('(');
609
+ if (startIdx === -1)
610
+ return '';
611
+ let depth = 0;
612
+ let endIdx = -1;
613
+ for (let i = startIdx; i < funcDef.length; i++) {
614
+ const char = funcDef[i];
615
+ if (char === '(') {
616
+ depth++;
617
+ }
618
+ else if (char === ')') {
619
+ depth--;
620
+ if (depth === 0) {
621
+ endIdx = i;
622
+ break;
623
+ }
624
+ }
625
+ }
626
+ if (endIdx === -1)
627
+ return '';
628
+ return funcDef.slice(startIdx + 1, endIdx);
629
+ }
630
+ /**
631
+ * Parse function parameters
632
+ */
633
+ parseParams(paramsStr) {
634
+ const params = [];
635
+ if (!paramsStr.trim())
636
+ return params;
637
+ // Split by comma, but respect nested brackets
638
+ const parts = this.splitParams(paramsStr);
639
+ for (const part of parts) {
640
+ const trimmed = part.trim();
641
+ if (!trimmed || trimmed === 'self' || trimmed === 'cls')
642
+ continue;
643
+ // Parse parameter: name: Type = default
644
+ const param = this.parseParam(trimmed);
645
+ if (param) {
646
+ params.push(param);
647
+ }
648
+ }
649
+ return params;
650
+ }
651
+ /**
652
+ * Split parameters respecting nested brackets
653
+ */
654
+ splitParams(paramsStr) {
655
+ const parts = [];
656
+ let current = '';
657
+ let depth = 0;
658
+ for (const char of paramsStr) {
659
+ if (char === '[' || char === '(' || char === '{') {
660
+ depth++;
661
+ current += char;
662
+ }
663
+ else if (char === ']' || char === ')' || char === '}') {
664
+ depth--;
665
+ current += char;
666
+ }
667
+ else if (char === ',' && depth === 0) {
668
+ parts.push(current);
669
+ current = '';
670
+ }
671
+ else {
672
+ current += char;
673
+ }
674
+ }
675
+ if (current.trim()) {
676
+ parts.push(current);
677
+ }
678
+ return parts;
679
+ }
680
+ /**
681
+ * Parse a single parameter
682
+ */
683
+ parseParam(paramStr) {
684
+ // Handle *args, **kwargs
685
+ if (paramStr.startsWith('*'))
686
+ return null;
687
+ // Split by = for default value
688
+ const eqIndex = paramStr.indexOf('=');
689
+ let nameType = paramStr;
690
+ let defaultValue;
691
+ if (eqIndex > 0) {
692
+ nameType = paramStr.slice(0, eqIndex).trim();
693
+ defaultValue = paramStr.slice(eqIndex + 1).trim();
694
+ }
695
+ // Split by : for type annotation
696
+ const colonIndex = nameType.indexOf(':');
697
+ let name = nameType;
698
+ let type;
699
+ if (colonIndex > 0) {
700
+ name = nameType.slice(0, colonIndex).trim();
701
+ type = nameType.slice(colonIndex + 1).trim();
702
+ }
703
+ return { name, type, default: defaultValue };
704
+ }
705
+ /**
706
+ * Extract parameters with source detection
707
+ */
708
+ extractParameters(params, path, flaskPathParams) {
709
+ const result = [];
710
+ // Extract path parameters from path
711
+ const pathParams = new Set();
712
+ const pathParamPattern = /\{(\w+)\}/g;
713
+ let match;
714
+ while ((match = pathParamPattern.exec(path)) !== null) {
715
+ pathParams.add(match[1]);
716
+ }
717
+ for (const param of params) {
718
+ let source = 'unknown';
719
+ let typeSchema = this.typeResolver.resolve(param.type);
720
+ let converter;
721
+ // Check if it's a path parameter
722
+ if (pathParams.has(param.name)) {
723
+ source = 'path';
724
+ // For Flask, check if there's a converter
725
+ if (flaskPathParams?.has(param.name)) {
726
+ converter = flaskPathParams.get(param.name);
727
+ typeSchema = this.flaskConverterToSchema(converter);
728
+ }
729
+ }
730
+ // Check if it's a FastAPI dependency pattern
731
+ else if (param.default?.includes('Query(')) {
732
+ source = 'query';
733
+ }
734
+ else if (param.default?.includes('Body(')) {
735
+ source = 'body';
736
+ }
737
+ else if (param.default?.includes('Header(')) {
738
+ source = 'header';
739
+ // Convert parameter name from snake_case to header format
740
+ }
741
+ else if (param.default?.includes('Depends(')) {
742
+ source = 'depends';
743
+ }
744
+ else if (param.default?.includes('Path(')) {
745
+ source = 'path';
746
+ }
747
+ // Default to query for primitive types, body for complex
748
+ else if (!pathParams.has(param.name)) {
749
+ const isPrimitive = ['str', 'int', 'float', 'bool'].some(t => param.type?.includes(t) || !param.type);
750
+ source = isPrimitive ? 'query' : 'body';
751
+ }
752
+ // Extract default value
753
+ let defaultValue;
754
+ if (param.default) {
755
+ defaultValue = this.parseDefaultValue(param.default);
756
+ }
757
+ result.push({
758
+ name: param.name,
759
+ type: param.type,
760
+ typeSchema,
761
+ required: param.default === undefined,
762
+ default: defaultValue,
763
+ source,
764
+ converter
765
+ });
766
+ }
767
+ return result;
768
+ }
769
+ /**
770
+ * Convert Flask URL converter to JSON Schema
771
+ */
772
+ flaskConverterToSchema(converter) {
773
+ switch (converter) {
774
+ case 'int':
775
+ return { type: 'integer' };
776
+ case 'float':
777
+ return { type: 'number' };
778
+ case 'path':
779
+ return { type: 'string' };
780
+ case 'uuid':
781
+ return { type: 'string', format: 'uuid' };
782
+ case 'string':
783
+ default:
784
+ return { type: 'string' };
785
+ }
786
+ }
787
+ /**
788
+ * Parse default value
789
+ */
790
+ parseDefaultValue(defaultStr) {
791
+ const trimmed = defaultStr.trim();
792
+ // None
793
+ if (trimmed === 'None')
794
+ return undefined;
795
+ // Boolean
796
+ if (trimmed === 'True')
797
+ return true;
798
+ if (trimmed === 'False')
799
+ return false;
800
+ // Number
801
+ if (/^-?\d+$/.test(trimmed))
802
+ return parseInt(trimmed, 10);
803
+ if (/^-?\d+\.\d+$/.test(trimmed))
804
+ return parseFloat(trimmed);
805
+ // String
806
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
807
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
808
+ return trimmed.slice(1, -1);
809
+ }
810
+ // Empty list/dict
811
+ if (trimmed === '[]')
812
+ return [];
813
+ if (trimmed === '{}')
814
+ return {};
815
+ // FastAPI Query/Body/etc - extract default from inside
816
+ const fastapiMatch = trimmed.match(/(?:Query|Body|Path|Header)\s*\(([^)]*)\)/);
817
+ if (fastapiMatch) {
818
+ const inner = fastapiMatch[1];
819
+ // First positional arg is the default
820
+ if (inner.startsWith('...'))
821
+ return undefined; // Required
822
+ const firstArg = inner.split(',')[0].trim();
823
+ return this.parseDefaultValue(firstArg);
824
+ }
825
+ return undefined;
826
+ }
827
+ /**
828
+ * Build input schema from parameters
829
+ */
830
+ buildInputSchema(params) {
831
+ const properties = {};
832
+ const required = [];
833
+ for (const param of params) {
834
+ if (param.source === 'depends')
835
+ continue; // Skip dependencies
836
+ properties[param.name] = {
837
+ ...param.typeSchema,
838
+ ...(param.default !== undefined && { default: param.default }),
839
+ ...(param.description && { description: param.description })
840
+ };
841
+ if (param.required) {
842
+ required.push(param.name);
843
+ }
844
+ }
845
+ return {
846
+ type: 'object',
847
+ properties,
848
+ required // Always include required array (empty or with values)
849
+ };
850
+ }
851
+ /**
852
+ * Extract docstring from function body
853
+ */
854
+ extractDocstring(body) {
855
+ const match = body.match(/^\s*(?:"""([\s\S]*?)"""|'''([\s\S]*?)''')/);
856
+ if (match) {
857
+ const docstring = (match[1] || match[2]).trim();
858
+ // Return first line or first sentence for short description
859
+ const firstLine = docstring.split('\n')[0].trim();
860
+ if (firstLine.includes('.')) {
861
+ return firstLine.split('.')[0].trim() + '.';
862
+ }
863
+ return firstLine;
864
+ }
865
+ return undefined;
866
+ }
867
+ /**
868
+ * Extract parameter descriptions from docstring Args section
869
+ */
870
+ extractParamDescriptions(body) {
871
+ const descriptions = new Map();
872
+ const match = body.match(/Args:\s*([\s\S]*?)(?:Returns:|Raises:|Example:|$)/);
873
+ if (match) {
874
+ const argsSection = match[1];
875
+ const paramPattern = /(\w+):\s*(.+?)(?=\n\s+\w+:|$)/gs;
876
+ let paramMatch;
877
+ while ((paramMatch = paramPattern.exec(argsSection)) !== null) {
878
+ const paramName = paramMatch[1];
879
+ const description = paramMatch[2].replace(/\n\s+/g, ' ').trim();
880
+ descriptions.set(paramName, description);
881
+ }
882
+ }
883
+ return descriptions;
884
+ }
885
+ /**
886
+ * Extract Pydantic models - uses two-pass approach for proper inheritance
887
+ */
888
+ extractPydanticModels(content, lines) {
889
+ // First pass: collect all model info with their own fields (but not inherited)
890
+ const modelInfos = [];
891
+ // Find class definitions that extend BaseModel
892
+ const classPattern = /class\s+(\w+)\s*\(([^)]+)\)\s*:/g;
893
+ let match;
894
+ while ((match = classPattern.exec(content)) !== null) {
895
+ const className = match[1];
896
+ const bases = match[2].split(',').map(b => b.trim());
897
+ // Check if it extends BaseModel (directly or indirectly)
898
+ const isBaseModel = bases.some(b => b === 'BaseModel' ||
899
+ b.includes('BaseModel') ||
900
+ this.isSubclassOfBaseModel(b));
901
+ if (!isBaseModel)
902
+ continue;
903
+ const lineNum = this.getLineNumber(content, match.index);
904
+ const classBody = this.extractClassBody(content, match.index + match[0].length, lines, lineNum);
905
+ // Parse fields (own fields only, not inherited)
906
+ const fields = this.extractPydanticFields(classBody, bases);
907
+ // Extract docstring
908
+ const docstring = this.extractDocstring(classBody);
909
+ // Register model with its own fields for type resolution and inheritance
910
+ // Keep full bases list (including 'BaseModel') for isSubclassOfBaseModel to work
911
+ const model = {
912
+ name: className,
913
+ fields,
914
+ bases, // Keep all bases for inheritance checking
915
+ docstring,
916
+ location: { file: '', line: lineNum, column: 0 }
917
+ };
918
+ this.typeResolver.registerModel(model);
919
+ modelInfos.push({ className, bases, fields, docstring, lineNum });
920
+ }
921
+ // Second pass: build schemas with inheritance resolved
922
+ const schemas = [];
923
+ for (const info of modelInfos) {
924
+ const properties = {};
925
+ const required = [];
926
+ // Collect inherited fields recursively
927
+ const collectInheritedFields = (bases) => {
928
+ for (const base of bases) {
929
+ if (base === 'BaseModel')
930
+ continue;
931
+ const baseModel = this.typeResolver.getModel(base);
932
+ if (baseModel) {
933
+ // First collect from parent's bases (grandparents)
934
+ collectInheritedFields(baseModel.bases);
935
+ // Then add parent's own fields
936
+ for (const field of baseModel.fields) {
937
+ properties[field.name] = field.typeSchema;
938
+ if (field.required && !required.includes(field.name)) {
939
+ required.push(field.name);
940
+ }
941
+ }
942
+ }
943
+ }
944
+ };
945
+ // Collect inherited fields
946
+ collectInheritedFields(info.bases);
947
+ // Add own fields (may override inherited)
948
+ for (const field of info.fields) {
949
+ properties[field.name] = {
950
+ ...field.typeSchema,
951
+ ...(field.default !== undefined && { default: field.default }),
952
+ ...(field.description && { description: field.description }),
953
+ ...(field.constraints?.minLength !== undefined && { minLength: field.constraints.minLength }),
954
+ ...(field.constraints?.maxLength !== undefined && { maxLength: field.constraints.maxLength }),
955
+ ...(field.constraints?.minimum !== undefined && { minimum: field.constraints.minimum }),
956
+ ...(field.constraints?.maximum !== undefined && { maximum: field.constraints.maximum }),
957
+ ...(field.constraints?.pattern && { pattern: field.constraints.pattern }),
958
+ };
959
+ if (field.required && !required.includes(field.name)) {
960
+ required.push(field.name);
961
+ }
962
+ }
963
+ schemas.push({
964
+ id: info.className,
965
+ toolName: info.className,
966
+ type: 'model',
967
+ properties,
968
+ required,
969
+ inputSchema: { type: 'object', properties, required: required.length > 0 ? required : undefined },
970
+ outputSchema: {},
971
+ description: info.docstring,
972
+ location: { file: '', line: info.lineNum, column: 0 }
973
+ });
974
+ }
975
+ return schemas;
976
+ }
977
+ /**
978
+ * Check if a class is a subclass of BaseModel
979
+ */
980
+ isSubclassOfBaseModel(className) {
981
+ const model = this.typeResolver.getModel(className);
982
+ if (!model)
983
+ return false;
984
+ return model.bases.some(b => b === 'BaseModel' || this.isSubclassOfBaseModel(b));
985
+ }
986
+ /**
987
+ * Extract class body
988
+ */
989
+ extractClassBody(content, startOffset, lines, startLineNum) {
990
+ // Find the indentation of the class body
991
+ const contentAfter = content.slice(startOffset);
992
+ const bodyMatch = contentAfter.match(/^[^\n]*\n([\s\S]*?)(?=\nclass\s|\n[^\s]|$)/);
993
+ if (bodyMatch) {
994
+ return bodyMatch[1];
995
+ }
996
+ return '';
997
+ }
998
+ /**
999
+ * Extract Pydantic field definitions
1000
+ */
1001
+ extractPydanticFields(classBody, bases) {
1002
+ const fields = [];
1003
+ // Pattern for field definitions: name: Type = default
1004
+ const fieldPattern = /^\s+(\w+)\s*:\s*([^=\n]+)(?:\s*=\s*(.+))?$/gm;
1005
+ let match;
1006
+ while ((match = fieldPattern.exec(classBody)) !== null) {
1007
+ const name = match[1];
1008
+ const typeStr = match[2].trim();
1009
+ const defaultStr = match[3]?.trim();
1010
+ // Skip methods and private fields
1011
+ if (name.startsWith('_') || name === 'Config')
1012
+ continue;
1013
+ // Parse type
1014
+ const typeSchema = this.typeResolver.resolve(typeStr);
1015
+ // Determine if required
1016
+ let required = true;
1017
+ let defaultValue;
1018
+ let constraints;
1019
+ let description;
1020
+ if (defaultStr) {
1021
+ required = false;
1022
+ // Check for Field() with constraints
1023
+ if (defaultStr.startsWith('Field(')) {
1024
+ const fieldInfo = this.parseFieldCall(defaultStr);
1025
+ required = fieldInfo.required;
1026
+ defaultValue = fieldInfo.default;
1027
+ constraints = fieldInfo.constraints;
1028
+ description = fieldInfo.description;
1029
+ }
1030
+ else {
1031
+ defaultValue = this.parseDefaultValue(defaultStr);
1032
+ // If default is None and type is Optional, it's not required
1033
+ if (defaultStr === 'None') {
1034
+ required = false;
1035
+ }
1036
+ }
1037
+ }
1038
+ // Check if type is Optional - then not required
1039
+ if (typeStr.startsWith('Optional[') || typeStr.includes(' | None')) {
1040
+ required = false;
1041
+ }
1042
+ fields.push({
1043
+ name,
1044
+ type: typeStr,
1045
+ typeSchema,
1046
+ required,
1047
+ default: defaultValue,
1048
+ description,
1049
+ constraints
1050
+ });
1051
+ }
1052
+ return fields;
1053
+ }
1054
+ /**
1055
+ * Parse Field() call for constraints
1056
+ */
1057
+ parseFieldCall(fieldStr) {
1058
+ const result = { required: true };
1059
+ // Extract arguments
1060
+ const argsMatch = fieldStr.match(/Field\s*\(([^)]*)\)/s);
1061
+ if (!argsMatch)
1062
+ return result;
1063
+ const args = argsMatch[1];
1064
+ // Check for ... (required marker)
1065
+ if (args.trim().startsWith('...')) {
1066
+ result.required = true;
1067
+ }
1068
+ else {
1069
+ // First positional arg is default
1070
+ const firstArg = args.split(',')[0].trim();
1071
+ if (firstArg && firstArg !== '...' && !firstArg.includes('=')) {
1072
+ result.default = this.parseDefaultValue(firstArg);
1073
+ result.required = false;
1074
+ }
1075
+ }
1076
+ // Parse keyword arguments
1077
+ const constraints = {};
1078
+ const minLengthMatch = args.match(/min_length\s*=\s*(\d+)/);
1079
+ if (minLengthMatch)
1080
+ constraints.minLength = parseInt(minLengthMatch[1]);
1081
+ const maxLengthMatch = args.match(/max_length\s*=\s*(\d+)/);
1082
+ if (maxLengthMatch)
1083
+ constraints.maxLength = parseInt(maxLengthMatch[1]);
1084
+ const geMatch = args.match(/ge\s*=\s*(-?\d+(?:\.\d+)?)/);
1085
+ if (geMatch)
1086
+ constraints.minimum = parseFloat(geMatch[1]);
1087
+ const leMatch = args.match(/le\s*=\s*(-?\d+(?:\.\d+)?)/);
1088
+ if (leMatch)
1089
+ constraints.maximum = parseFloat(leMatch[1]);
1090
+ const patternMatch = args.match(/pattern\s*=\s*["']([^"']+)["']/);
1091
+ if (patternMatch)
1092
+ constraints.pattern = patternMatch[1];
1093
+ const minItemsMatch = args.match(/min_items\s*=\s*(\d+)/);
1094
+ if (minItemsMatch)
1095
+ constraints.minItems = parseInt(minItemsMatch[1]);
1096
+ const maxItemsMatch = args.match(/max_items\s*=\s*(\d+)/);
1097
+ if (maxItemsMatch)
1098
+ constraints.maxItems = parseInt(maxItemsMatch[1]);
1099
+ if (Object.keys(constraints).length > 0) {
1100
+ result.constraints = constraints;
1101
+ }
1102
+ // Description
1103
+ const descMatch = args.match(/description\s*=\s*["']([^"']+)["']/);
1104
+ if (descMatch) {
1105
+ result.description = descMatch[1];
1106
+ }
1107
+ return result;
1108
+ }
1109
+ /**
1110
+ * Get line number for a character offset
1111
+ */
1112
+ getLineNumber(content, offset) {
1113
+ const beforeOffset = content.slice(0, offset);
1114
+ return (beforeOffset.match(/\n/g) || []).length + 1;
1115
+ }
1116
+ /**
1117
+ * Extract typed functions (for type annotation testing)
1118
+ * This extracts functions that have type annotations but are NOT decorated
1119
+ */
1120
+ extractTypedFunctions(content, lines) {
1121
+ const schemas = [];
1122
+ // Pattern to match undecorated function definitions with type hints
1123
+ // def func_name(param: type, ...) -> return_type:
1124
+ const funcPattern = /^(async\s+)?def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*([^:]+))?\s*:/gm;
1125
+ let match;
1126
+ while ((match = funcPattern.exec(content)) !== null) {
1127
+ const isAsync = !!match[1];
1128
+ const funcName = match[2];
1129
+ const paramsStr = match[3];
1130
+ const returnType = match[4]?.trim();
1131
+ const lineNum = this.getLineNumber(content, match.index);
1132
+ // Check if this function is decorated (look at previous line)
1133
+ const prevLineIdx = lineNum - 2;
1134
+ if (prevLineIdx >= 0 && lines[prevLineIdx]?.trim().startsWith('@')) {
1135
+ // Skip decorated functions - they're handled elsewhere
1136
+ continue;
1137
+ }
1138
+ // Skip dunder methods
1139
+ if (funcName.startsWith('__') && funcName.endsWith('__')) {
1140
+ continue;
1141
+ }
1142
+ // Skip methods inside classes (check indentation)
1143
+ const funcLine = lines[lineNum - 1];
1144
+ if (funcLine && this.getIndent(funcLine) > 0) {
1145
+ continue;
1146
+ }
1147
+ // Parse parameters
1148
+ const params = this.parseParams(paramsStr);
1149
+ // Skip functions without type hints
1150
+ const hasTypeHints = params.some(p => p.type) || returnType;
1151
+ if (!hasTypeHints)
1152
+ continue;
1153
+ // Build input schema from parameters
1154
+ const properties = {};
1155
+ const required = [];
1156
+ for (const param of params) {
1157
+ if (param.type) {
1158
+ const typeSchema = this.typeResolver.resolve(param.type);
1159
+ properties[param.name] = typeSchema;
1160
+ if (param.default === undefined) {
1161
+ required.push(param.name);
1162
+ }
1163
+ }
1164
+ }
1165
+ // Build output schema from return type
1166
+ const outputSchema = returnType ? this.typeResolver.resolve(returnType) : {};
1167
+ schemas.push({
1168
+ id: funcName,
1169
+ toolName: funcName,
1170
+ async: isAsync,
1171
+ type: 'function',
1172
+ inputSchema: {
1173
+ type: 'object',
1174
+ properties,
1175
+ ...(required.length > 0 && { required })
1176
+ },
1177
+ outputSchema,
1178
+ location: { file: '', line: lineNum, column: 0 }
1179
+ });
1180
+ }
1181
+ return schemas;
1182
+ }
1183
+ /**
1184
+ * Normalize a path by ensuring leading slash and removing trailing slashes
1185
+ */
1186
+ normalizePath(path) {
1187
+ // Handle empty/falsy path
1188
+ if (!path || path.trim() === '') {
1189
+ return '/';
1190
+ }
1191
+ // Trim whitespace, CRLF, and any hidden characters
1192
+ path = path.trim().replace(/[\r\n\t]/g, '');
1193
+ // Ensure leading slash
1194
+ if (!path.startsWith('/')) {
1195
+ path = '/' + path;
1196
+ }
1197
+ // Remove ALL trailing slashes and whitespace (but keep root "/" as is)
1198
+ // Use a more aggressive approach
1199
+ while (path.length > 1 && (path.endsWith('/') || path.endsWith(' ') || path.charCodeAt(path.length - 1) < 32)) {
1200
+ path = path.slice(0, -1);
1201
+ }
1202
+ return path || '/';
1203
+ }
1204
+ }
1205
+ //# sourceMappingURL=parser.js.map