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.
- package/LICENSE +21 -0
- package/README.md +1662 -0
- package/dist/adapters/bootstrap.d.ts +29 -0
- package/dist/adapters/bootstrap.d.ts.map +1 -0
- package/dist/adapters/bootstrap.js +46 -0
- package/dist/adapters/bootstrap.js.map +1 -0
- package/dist/adapters/errors.d.ts +94 -0
- package/dist/adapters/errors.d.ts.map +1 -0
- package/dist/adapters/errors.js +107 -0
- package/dist/adapters/errors.js.map +1 -0
- package/dist/adapters/graphql/index.d.ts +9 -0
- package/dist/adapters/graphql/index.d.ts.map +1 -0
- package/dist/adapters/graphql/index.js +9 -0
- package/dist/adapters/graphql/index.js.map +1 -0
- package/dist/adapters/graphql/sdl-parser.d.ts +74 -0
- package/dist/adapters/graphql/sdl-parser.d.ts.map +1 -0
- package/dist/adapters/graphql/sdl-parser.js +559 -0
- package/dist/adapters/graphql/sdl-parser.js.map +1 -0
- package/dist/adapters/grpc/adapter.d.ts +76 -0
- package/dist/adapters/grpc/adapter.d.ts.map +1 -0
- package/dist/adapters/grpc/adapter.js +362 -0
- package/dist/adapters/grpc/adapter.js.map +1 -0
- package/dist/adapters/grpc/index.d.ts +10 -0
- package/dist/adapters/grpc/index.d.ts.map +1 -0
- package/dist/adapters/grpc/index.js +12 -0
- package/dist/adapters/grpc/index.js.map +1 -0
- package/dist/adapters/grpc/proto-parser.d.ts +76 -0
- package/dist/adapters/grpc/proto-parser.d.ts.map +1 -0
- package/dist/adapters/grpc/proto-parser.js +523 -0
- package/dist/adapters/grpc/proto-parser.js.map +1 -0
- package/dist/adapters/grpc/type-converter.d.ts +43 -0
- package/dist/adapters/grpc/type-converter.d.ts.map +1 -0
- package/dist/adapters/grpc/type-converter.js +270 -0
- package/dist/adapters/grpc/type-converter.js.map +1 -0
- package/dist/adapters/grpc/types.d.ts +85 -0
- package/dist/adapters/grpc/types.d.ts.map +1 -0
- package/dist/adapters/grpc/types.js +7 -0
- package/dist/adapters/grpc/types.js.map +1 -0
- package/dist/adapters/index.d.ts +39 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +50 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mcp.d.ts +23 -0
- package/dist/adapters/mcp.d.ts.map +1 -0
- package/dist/adapters/mcp.js +293 -0
- package/dist/adapters/mcp.js.map +1 -0
- package/dist/adapters/openapi/adapter.d.ts +213 -0
- package/dist/adapters/openapi/adapter.d.ts.map +1 -0
- package/dist/adapters/openapi/adapter.js +557 -0
- package/dist/adapters/openapi/adapter.js.map +1 -0
- package/dist/adapters/openapi/convert.d.ts +120 -0
- package/dist/adapters/openapi/convert.d.ts.map +1 -0
- package/dist/adapters/openapi/convert.js +363 -0
- package/dist/adapters/openapi/convert.js.map +1 -0
- package/dist/adapters/openapi/index.d.ts +39 -0
- package/dist/adapters/openapi/index.d.ts.map +1 -0
- package/dist/adapters/openapi/index.js +48 -0
- package/dist/adapters/openapi/index.js.map +1 -0
- package/dist/adapters/openapi/parser.d.ts +95 -0
- package/dist/adapters/openapi/parser.d.ts.map +1 -0
- package/dist/adapters/openapi/parser.js +171 -0
- package/dist/adapters/openapi/parser.js.map +1 -0
- package/dist/adapters/registry.d.ts +116 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +246 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/trpc/adapter.d.ts +159 -0
- package/dist/adapters/trpc/adapter.d.ts.map +1 -0
- package/dist/adapters/trpc/adapter.js +223 -0
- package/dist/adapters/trpc/adapter.js.map +1 -0
- package/dist/adapters/trpc/extractor.d.ts +218 -0
- package/dist/adapters/trpc/extractor.d.ts.map +1 -0
- package/dist/adapters/trpc/extractor.js +708 -0
- package/dist/adapters/trpc/extractor.js.map +1 -0
- package/dist/adapters/trpc/index.d.ts +31 -0
- package/dist/adapters/trpc/index.d.ts.map +1 -0
- package/dist/adapters/trpc/index.js +40 -0
- package/dist/adapters/trpc/index.js.map +1 -0
- package/dist/adapters/trpc/parser.d.ts +119 -0
- package/dist/adapters/trpc/parser.d.ts.map +1 -0
- package/dist/adapters/trpc/parser.js +128 -0
- package/dist/adapters/trpc/parser.js.map +1 -0
- package/dist/compare/index.d.ts +33 -0
- package/dist/compare/index.d.ts.map +1 -0
- package/dist/compare/index.js +261 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/core/types.d.ts +188 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/extract/index.d.ts +26 -0
- package/dist/extract/index.d.ts.map +1 -0
- package/dist/extract/index.js +44 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +674 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/base.d.ts +57 -0
- package/dist/languages/base.d.ts.map +1 -0
- package/dist/languages/base.js +6 -0
- package/dist/languages/base.js.map +1 -0
- package/dist/languages/bootstrap.d.ts +10 -0
- package/dist/languages/bootstrap.d.ts.map +1 -0
- package/dist/languages/bootstrap.js +25 -0
- package/dist/languages/bootstrap.js.map +1 -0
- package/dist/languages/go/handlers/chi.d.ts +24 -0
- package/dist/languages/go/handlers/chi.d.ts.map +1 -0
- package/dist/languages/go/handlers/chi.js +205 -0
- package/dist/languages/go/handlers/chi.js.map +1 -0
- package/dist/languages/go/handlers/gin.d.ts +24 -0
- package/dist/languages/go/handlers/gin.d.ts.map +1 -0
- package/dist/languages/go/handlers/gin.js +156 -0
- package/dist/languages/go/handlers/gin.js.map +1 -0
- package/dist/languages/go/handlers/stdlib.d.ts +19 -0
- package/dist/languages/go/handlers/stdlib.d.ts.map +1 -0
- package/dist/languages/go/handlers/stdlib.js +112 -0
- package/dist/languages/go/handlers/stdlib.js.map +1 -0
- package/dist/languages/go/index.d.ts +18 -0
- package/dist/languages/go/index.d.ts.map +1 -0
- package/dist/languages/go/index.js +20 -0
- package/dist/languages/go/index.js.map +1 -0
- package/dist/languages/go/parser.d.ts +33 -0
- package/dist/languages/go/parser.d.ts.map +1 -0
- package/dist/languages/go/parser.js +95 -0
- package/dist/languages/go/parser.js.map +1 -0
- package/dist/languages/go/struct-extractor.d.ts +59 -0
- package/dist/languages/go/struct-extractor.d.ts.map +1 -0
- package/dist/languages/go/struct-extractor.js +483 -0
- package/dist/languages/go/struct-extractor.js.map +1 -0
- package/dist/languages/go/tag-parser.d.ts +62 -0
- package/dist/languages/go/tag-parser.d.ts.map +1 -0
- package/dist/languages/go/tag-parser.js +108 -0
- package/dist/languages/go/tag-parser.js.map +1 -0
- package/dist/languages/go/type-converter.d.ts +32 -0
- package/dist/languages/go/type-converter.d.ts.map +1 -0
- package/dist/languages/go/type-converter.js +226 -0
- package/dist/languages/go/type-converter.js.map +1 -0
- package/dist/languages/go/types.d.ts +153 -0
- package/dist/languages/go/types.d.ts.map +1 -0
- package/dist/languages/go/types.js +6 -0
- package/dist/languages/go/types.js.map +1 -0
- package/dist/languages/import-resolver.d.ts +645 -0
- package/dist/languages/import-resolver.d.ts.map +1 -0
- package/dist/languages/import-resolver.js +1278 -0
- package/dist/languages/import-resolver.js.map +1 -0
- package/dist/languages/index.d.ts +34 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +93 -0
- package/dist/languages/index.js.map +1 -0
- package/dist/languages/json-schema.d.ts +40 -0
- package/dist/languages/json-schema.d.ts.map +1 -0
- package/dist/languages/json-schema.js +188 -0
- package/dist/languages/json-schema.js.map +1 -0
- package/dist/languages/python-ast/index.d.ts +8 -0
- package/dist/languages/python-ast/index.d.ts.map +1 -0
- package/dist/languages/python-ast/index.js +7 -0
- package/dist/languages/python-ast/index.js.map +1 -0
- package/dist/languages/python-ast/parser.d.ts +174 -0
- package/dist/languages/python-ast/parser.d.ts.map +1 -0
- package/dist/languages/python-ast/parser.js +1205 -0
- package/dist/languages/python-ast/parser.js.map +1 -0
- package/dist/languages/python-ast/type-resolver.d.ts +75 -0
- package/dist/languages/python-ast/type-resolver.d.ts.map +1 -0
- package/dist/languages/python-ast/type-resolver.js +421 -0
- package/dist/languages/python-ast/type-resolver.js.map +1 -0
- package/dist/languages/python-ast/types.d.ts +216 -0
- package/dist/languages/python-ast/types.d.ts.map +1 -0
- package/dist/languages/python-ast/types.js +6 -0
- package/dist/languages/python-ast/types.js.map +1 -0
- package/dist/languages/python.d.ts +55 -0
- package/dist/languages/python.d.ts.map +1 -0
- package/dist/languages/python.js +311 -0
- package/dist/languages/python.js.map +1 -0
- package/dist/languages/typescript.d.ts +272 -0
- package/dist/languages/typescript.d.ts.map +1 -0
- package/dist/languages/typescript.js +1381 -0
- package/dist/languages/typescript.js.map +1 -0
- package/dist/patterns/base.d.ts +146 -0
- package/dist/patterns/base.d.ts.map +1 -0
- package/dist/patterns/base.js +89 -0
- package/dist/patterns/base.js.map +1 -0
- package/dist/patterns/errors.d.ts +172 -0
- package/dist/patterns/errors.d.ts.map +1 -0
- package/dist/patterns/errors.js +185 -0
- package/dist/patterns/errors.js.map +1 -0
- package/dist/patterns/extractors.d.ts +170 -0
- package/dist/patterns/extractors.d.ts.map +1 -0
- package/dist/patterns/extractors.js +305 -0
- package/dist/patterns/extractors.js.map +1 -0
- package/dist/patterns/graphql/apollo-client.d.ts +80 -0
- package/dist/patterns/graphql/apollo-client.d.ts.map +1 -0
- package/dist/patterns/graphql/apollo-client.js +800 -0
- package/dist/patterns/graphql/apollo-client.js.map +1 -0
- package/dist/patterns/graphql/apollo-server.d.ts +55 -0
- package/dist/patterns/graphql/apollo-server.d.ts.map +1 -0
- package/dist/patterns/graphql/apollo-server.js +523 -0
- package/dist/patterns/graphql/apollo-server.js.map +1 -0
- package/dist/patterns/graphql/index.d.ts +11 -0
- package/dist/patterns/graphql/index.d.ts.map +1 -0
- package/dist/patterns/graphql/index.js +12 -0
- package/dist/patterns/graphql/index.js.map +1 -0
- package/dist/patterns/graphql/types.d.ts +213 -0
- package/dist/patterns/graphql/types.d.ts.map +1 -0
- package/dist/patterns/graphql/types.js +16 -0
- package/dist/patterns/graphql/types.js.map +1 -0
- package/dist/patterns/http-clients/axios.d.ts +148 -0
- package/dist/patterns/http-clients/axios.d.ts.map +1 -0
- package/dist/patterns/http-clients/axios.js +652 -0
- package/dist/patterns/http-clients/axios.js.map +1 -0
- package/dist/patterns/http-clients/fetch.d.ts +88 -0
- package/dist/patterns/http-clients/fetch.d.ts.map +1 -0
- package/dist/patterns/http-clients/fetch.js +364 -0
- package/dist/patterns/http-clients/fetch.js.map +1 -0
- package/dist/patterns/http-clients/index.d.ts +36 -0
- package/dist/patterns/http-clients/index.d.ts.map +1 -0
- package/dist/patterns/http-clients/index.js +50 -0
- package/dist/patterns/http-clients/index.js.map +1 -0
- package/dist/patterns/http-clients/property-access.d.ts +46 -0
- package/dist/patterns/http-clients/property-access.d.ts.map +1 -0
- package/dist/patterns/http-clients/property-access.js +818 -0
- package/dist/patterns/http-clients/property-access.js.map +1 -0
- package/dist/patterns/http-clients/type-inference.d.ts +48 -0
- package/dist/patterns/http-clients/type-inference.d.ts.map +1 -0
- package/dist/patterns/http-clients/type-inference.js +293 -0
- package/dist/patterns/http-clients/type-inference.js.map +1 -0
- package/dist/patterns/http-clients/types.d.ts +168 -0
- package/dist/patterns/http-clients/types.d.ts.map +1 -0
- package/dist/patterns/http-clients/types.js +10 -0
- package/dist/patterns/http-clients/types.js.map +1 -0
- package/dist/patterns/http-clients/url-extractor.d.ts +53 -0
- package/dist/patterns/http-clients/url-extractor.d.ts.map +1 -0
- package/dist/patterns/http-clients/url-extractor.js +338 -0
- package/dist/patterns/http-clients/url-extractor.js.map +1 -0
- package/dist/patterns/index.d.ts +44 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +49 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/patterns/python/aiohttp.d.ts +21 -0
- package/dist/patterns/python/aiohttp.d.ts.map +1 -0
- package/dist/patterns/python/aiohttp.js +188 -0
- package/dist/patterns/python/aiohttp.js.map +1 -0
- package/dist/patterns/python/httpx.d.ts +20 -0
- package/dist/patterns/python/httpx.d.ts.map +1 -0
- package/dist/patterns/python/httpx.js +183 -0
- package/dist/patterns/python/httpx.js.map +1 -0
- package/dist/patterns/python/index.d.ts +32 -0
- package/dist/patterns/python/index.d.ts.map +1 -0
- package/dist/patterns/python/index.js +63 -0
- package/dist/patterns/python/index.js.map +1 -0
- package/dist/patterns/python/property-access.d.ts +27 -0
- package/dist/patterns/python/property-access.d.ts.map +1 -0
- package/dist/patterns/python/property-access.js +132 -0
- package/dist/patterns/python/property-access.js.map +1 -0
- package/dist/patterns/python/requests.d.ts +19 -0
- package/dist/patterns/python/requests.d.ts.map +1 -0
- package/dist/patterns/python/requests.js +239 -0
- package/dist/patterns/python/requests.js.map +1 -0
- package/dist/patterns/python/types.d.ts +95 -0
- package/dist/patterns/python/types.d.ts.map +1 -0
- package/dist/patterns/python/types.js +43 -0
- package/dist/patterns/python/types.js.map +1 -0
- package/dist/patterns/registry.d.ts +181 -0
- package/dist/patterns/registry.d.ts.map +1 -0
- package/dist/patterns/registry.js +304 -0
- package/dist/patterns/registry.js.map +1 -0
- package/dist/patterns/rest/express.d.ts +78 -0
- package/dist/patterns/rest/express.d.ts.map +1 -0
- package/dist/patterns/rest/express.js +289 -0
- package/dist/patterns/rest/express.js.map +1 -0
- package/dist/patterns/rest/fastify.d.ts +93 -0
- package/dist/patterns/rest/fastify.d.ts.map +1 -0
- package/dist/patterns/rest/fastify.js +420 -0
- package/dist/patterns/rest/fastify.js.map +1 -0
- package/dist/patterns/rest/index.d.ts +31 -0
- package/dist/patterns/rest/index.d.ts.map +1 -0
- package/dist/patterns/rest/index.js +45 -0
- package/dist/patterns/rest/index.js.map +1 -0
- package/dist/patterns/rest/middleware.d.ts +25 -0
- package/dist/patterns/rest/middleware.d.ts.map +1 -0
- package/dist/patterns/rest/middleware.js +219 -0
- package/dist/patterns/rest/middleware.js.map +1 -0
- package/dist/patterns/rest/path-parser.d.ts +50 -0
- package/dist/patterns/rest/path-parser.d.ts.map +1 -0
- package/dist/patterns/rest/path-parser.js +137 -0
- package/dist/patterns/rest/path-parser.js.map +1 -0
- package/dist/patterns/rest/response-inference.d.ts +44 -0
- package/dist/patterns/rest/response-inference.d.ts.map +1 -0
- package/dist/patterns/rest/response-inference.js +218 -0
- package/dist/patterns/rest/response-inference.js.map +1 -0
- package/dist/patterns/rest/types.d.ts +102 -0
- package/dist/patterns/rest/types.d.ts.map +1 -0
- package/dist/patterns/rest/types.js +10 -0
- package/dist/patterns/rest/types.js.map +1 -0
- package/dist/patterns/types.d.ts +105 -0
- package/dist/patterns/types.d.ts.map +1 -0
- package/dist/patterns/types.js +11 -0
- package/dist/patterns/types.js.map +1 -0
- package/dist/report/index.d.ts +11 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +55 -0
- package/dist/report/index.js.map +1 -0
- package/dist/tools/contract-comments.d.ts +48 -0
- package/dist/tools/contract-comments.d.ts.map +1 -0
- package/dist/tools/contract-comments.js +130 -0
- package/dist/tools/contract-comments.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/scaffold.d.ts +38 -0
- package/dist/tools/scaffold.d.ts.map +1 -0
- package/dist/tools/scaffold.js +373 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/trace/index.d.ts +28 -0
- package/dist/trace/index.d.ts.map +1 -0
- package/dist/trace/index.js +45 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/watch/cache.d.ts +41 -0
- package/dist/watch/cache.d.ts.map +1 -0
- package/dist/watch/cache.js +230 -0
- package/dist/watch/cache.js.map +1 -0
- package/dist/watch/index.d.ts +9 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +7 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/project.d.ts +128 -0
- package/dist/watch/project.d.ts.map +1 -0
- package/dist/watch/project.js +152 -0
- package/dist/watch/project.js.map +1 -0
- package/dist/watch/watcher.d.ts +76 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +235 -0
- package/dist/watch/watcher.js.map +1 -0
- 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
|