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,1381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔷 TypeScript Language Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts MCP schemas from TypeScript source files using ts-morph.
|
|
5
|
+
* This is an enhanced parser that supports multiple extraction patterns
|
|
6
|
+
* for both producer schemas (tool definitions) and type definitions.
|
|
7
|
+
*
|
|
8
|
+
* Supported extraction patterns:
|
|
9
|
+
* 1. **server.tool()** - FastMCP / MCP SDK pattern
|
|
10
|
+
* 2. **Registry Pattern** - Object literal with ToolDefinition entries (ChatRPG)
|
|
11
|
+
* 3. **Exported Zod Schemas** - Named *Schema exports for tracing
|
|
12
|
+
* 4. **TypeScript Interfaces** - Exported interface declarations
|
|
13
|
+
* 5. **Type Aliases** - Exported type definitions
|
|
14
|
+
* 6. **Enums** - Exported enum declarations
|
|
15
|
+
*
|
|
16
|
+
* @module languages/typescript
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { TypeScriptParser } from './languages/typescript.js';
|
|
21
|
+
*
|
|
22
|
+
* const parser = new TypeScriptParser();
|
|
23
|
+
*
|
|
24
|
+
* // Extract tool definitions (Zod schemas)
|
|
25
|
+
* const producers = await parser.extractSchemas({ rootDir: './src' });
|
|
26
|
+
*
|
|
27
|
+
* // Extract interfaces and types
|
|
28
|
+
* const interfaces = await parser.extractInterfaces({ rootDir: './src' });
|
|
29
|
+
*
|
|
30
|
+
* // Extract all schemas
|
|
31
|
+
* const all = await parser.extractAll({ rootDir: './src' });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { Project, SyntaxKind, Node, ts } from 'ts-morph';
|
|
35
|
+
// Common registry patterns found in MCP servers
|
|
36
|
+
const REGISTRY_PATTERNS = [
|
|
37
|
+
{
|
|
38
|
+
name: 'toolRegistry',
|
|
39
|
+
type: 'record',
|
|
40
|
+
entryShape: {
|
|
41
|
+
nameField: 'name',
|
|
42
|
+
descriptionField: 'description',
|
|
43
|
+
schemaField: 'inputSchema',
|
|
44
|
+
handlerField: 'handler',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'tools',
|
|
49
|
+
type: 'array',
|
|
50
|
+
entryShape: {
|
|
51
|
+
nameField: 'name',
|
|
52
|
+
descriptionField: 'description',
|
|
53
|
+
schemaField: 'schema',
|
|
54
|
+
handlerField: 'handler',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'TOOLS',
|
|
59
|
+
type: 'record',
|
|
60
|
+
entryShape: {
|
|
61
|
+
nameField: 'name',
|
|
62
|
+
descriptionField: 'description',
|
|
63
|
+
schemaField: 'inputSchema',
|
|
64
|
+
handlerField: 'handler',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Parser Implementation
|
|
70
|
+
// ============================================================================
|
|
71
|
+
export class TypeScriptParser {
|
|
72
|
+
name = 'typescript';
|
|
73
|
+
filePatterns = ['**/*.ts', '**/*.tsx'];
|
|
74
|
+
// Track found Zod schemas for cross-file resolution
|
|
75
|
+
zodSchemaCache = new Map();
|
|
76
|
+
// ==========================================================================
|
|
77
|
+
// Producer Schema Extraction
|
|
78
|
+
// ==========================================================================
|
|
79
|
+
async extractSchemas(options) {
|
|
80
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
81
|
+
console.error(`[TypeScript] Scanning: ${options.rootDir}`);
|
|
82
|
+
}
|
|
83
|
+
const project = new Project({
|
|
84
|
+
tsConfigFilePath: undefined,
|
|
85
|
+
skipAddingFilesFromTsConfig: true,
|
|
86
|
+
});
|
|
87
|
+
const patterns = options.include || this.filePatterns;
|
|
88
|
+
const excludePatterns = options.exclude || ['**/node_modules/**', '**/dist/**'];
|
|
89
|
+
project.addSourceFilesAtPaths(patterns.map(p => `${options.rootDir}/${p}`));
|
|
90
|
+
const schemas = [];
|
|
91
|
+
// First pass: collect all exported Zod schemas for cross-reference
|
|
92
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
93
|
+
const filePath = sourceFile.getFilePath();
|
|
94
|
+
if (excludePatterns.some(pattern => filePath.includes(pattern.replace('**/', '')))) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
this.collectZodSchemas(sourceFile, filePath);
|
|
98
|
+
}
|
|
99
|
+
// Second pass: extract tool definitions
|
|
100
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
101
|
+
const filePath = sourceFile.getFilePath();
|
|
102
|
+
if (excludePatterns.some(pattern => filePath.includes(pattern.replace('**/', '')))) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Method 1: server.tool() calls (FastMCP pattern)
|
|
106
|
+
const toolCalls = this.findToolCalls(sourceFile);
|
|
107
|
+
for (const toolCall of toolCalls) {
|
|
108
|
+
const schema = this.parseToolCall(toolCall, filePath);
|
|
109
|
+
if (schema) {
|
|
110
|
+
schemas.push(schema);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Method 2: Registry pattern (ChatRPG pattern)
|
|
114
|
+
const registrySchemas = this.extractFromRegistry(sourceFile, filePath);
|
|
115
|
+
schemas.push(...registrySchemas);
|
|
116
|
+
// Method 3: Direct exports with tool-like shape
|
|
117
|
+
const exportedSchemas = this.extractFromExports(sourceFile, filePath);
|
|
118
|
+
schemas.push(...exportedSchemas);
|
|
119
|
+
}
|
|
120
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
121
|
+
console.error(`[TypeScript] Found ${schemas.length} tool definitions`);
|
|
122
|
+
}
|
|
123
|
+
return schemas;
|
|
124
|
+
}
|
|
125
|
+
// ==========================================================================
|
|
126
|
+
// Method 1: server.tool() Pattern (Original)
|
|
127
|
+
// ==========================================================================
|
|
128
|
+
findToolCalls(sourceFile) {
|
|
129
|
+
const toolCalls = [];
|
|
130
|
+
sourceFile.forEachDescendant((node) => {
|
|
131
|
+
if (Node.isCallExpression(node)) {
|
|
132
|
+
const expression = node.getExpression();
|
|
133
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
134
|
+
const methodName = expression.getName();
|
|
135
|
+
if (methodName === 'tool') {
|
|
136
|
+
toolCalls.push(node);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return toolCalls;
|
|
142
|
+
}
|
|
143
|
+
parseToolCall(callExpr, filePath) {
|
|
144
|
+
const args = callExpr.getArguments();
|
|
145
|
+
if (args.length < 3) {
|
|
146
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
147
|
+
console.error(`[TypeScript] Skipping tool call with insufficient args at ${filePath}:${callExpr.getStartLineNumber()}`);
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const nameArg = args[0];
|
|
152
|
+
let toolName = 'unknown';
|
|
153
|
+
if (Node.isStringLiteral(nameArg)) {
|
|
154
|
+
toolName = nameArg.getLiteralValue();
|
|
155
|
+
}
|
|
156
|
+
let description;
|
|
157
|
+
let schemaArg;
|
|
158
|
+
const secondArg = args[1];
|
|
159
|
+
if (Node.isStringLiteral(secondArg)) {
|
|
160
|
+
description = secondArg.getLiteralValue();
|
|
161
|
+
schemaArg = args[2];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
schemaArg = secondArg;
|
|
165
|
+
}
|
|
166
|
+
const inputSchema = this.parseZodSchema(schemaArg);
|
|
167
|
+
const location = {
|
|
168
|
+
file: filePath,
|
|
169
|
+
line: callExpr.getStartLineNumber(),
|
|
170
|
+
column: callExpr.getStartLinePos(),
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
toolName,
|
|
174
|
+
description,
|
|
175
|
+
inputSchema,
|
|
176
|
+
outputSchema: this.getMcpOutputSchema(),
|
|
177
|
+
location,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// Method 2: Registry Pattern (ChatRPG)
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
extractFromRegistry(sourceFile, filePath) {
|
|
184
|
+
const schemas = [];
|
|
185
|
+
// Look for exported object literals that match registry patterns
|
|
186
|
+
sourceFile.forEachDescendant((node) => {
|
|
187
|
+
if (Node.isVariableDeclaration(node)) {
|
|
188
|
+
const varName = node.getName();
|
|
189
|
+
const initializer = node.getInitializer();
|
|
190
|
+
// Check if this matches a known registry pattern
|
|
191
|
+
const pattern = REGISTRY_PATTERNS.find(p => p.name === varName);
|
|
192
|
+
if (pattern && initializer && Node.isObjectLiteralExpression(initializer)) {
|
|
193
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
194
|
+
console.error(`[TypeScript] Found registry pattern: ${varName}`);
|
|
195
|
+
}
|
|
196
|
+
const registrySchemas = this.parseRegistryObject(initializer, pattern, filePath);
|
|
197
|
+
schemas.push(...registrySchemas);
|
|
198
|
+
return; // Found, skip to next
|
|
199
|
+
}
|
|
200
|
+
// Also check for objects with tool-like entries even if not matching named patterns
|
|
201
|
+
if (initializer && Node.isObjectLiteralExpression(initializer)) {
|
|
202
|
+
const inferredSchemas = this.inferRegistryFromShape(initializer, varName, filePath);
|
|
203
|
+
if (inferredSchemas.length > 0) {
|
|
204
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
205
|
+
console.error(`[TypeScript] Inferred registry from shape: ${varName} (${inferredSchemas.length} tools)`);
|
|
206
|
+
}
|
|
207
|
+
schemas.push(...inferredSchemas);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return schemas;
|
|
213
|
+
}
|
|
214
|
+
parseRegistryObject(obj, pattern, filePath) {
|
|
215
|
+
const schemas = [];
|
|
216
|
+
for (const prop of obj.getProperties()) {
|
|
217
|
+
if (!Node.isPropertyAssignment(prop))
|
|
218
|
+
continue;
|
|
219
|
+
const entryName = prop.getName();
|
|
220
|
+
const entryValue = prop.getInitializer();
|
|
221
|
+
if (!entryValue || !Node.isObjectLiteralExpression(entryValue))
|
|
222
|
+
continue;
|
|
223
|
+
const toolDef = this.parseToolDefinition(entryValue, pattern.entryShape, filePath, prop.getStartLineNumber());
|
|
224
|
+
if (toolDef) {
|
|
225
|
+
// Use the property key as fallback name
|
|
226
|
+
if (toolDef.toolName === 'unknown') {
|
|
227
|
+
toolDef.toolName = entryName;
|
|
228
|
+
}
|
|
229
|
+
schemas.push(toolDef);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return schemas;
|
|
233
|
+
}
|
|
234
|
+
parseToolDefinition(obj, shape, filePath, lineNumber) {
|
|
235
|
+
let toolName = 'unknown';
|
|
236
|
+
let description;
|
|
237
|
+
let inputSchema = { type: 'object' };
|
|
238
|
+
for (const prop of obj.getProperties()) {
|
|
239
|
+
if (!Node.isPropertyAssignment(prop))
|
|
240
|
+
continue;
|
|
241
|
+
const propName = prop.getName();
|
|
242
|
+
const init = prop.getInitializer();
|
|
243
|
+
if (!init)
|
|
244
|
+
continue;
|
|
245
|
+
// Extract name
|
|
246
|
+
if (propName === shape.nameField && Node.isStringLiteral(init)) {
|
|
247
|
+
toolName = init.getLiteralValue();
|
|
248
|
+
}
|
|
249
|
+
// Extract description
|
|
250
|
+
if (propName === shape.descriptionField && Node.isStringLiteral(init)) {
|
|
251
|
+
description = init.getLiteralValue();
|
|
252
|
+
}
|
|
253
|
+
// Extract schema
|
|
254
|
+
if (propName === shape.schemaField) {
|
|
255
|
+
inputSchema = this.extractInputSchema(init);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Skip if we couldn't find a name or schema
|
|
259
|
+
if (toolName === 'unknown' && Object.keys(inputSchema.properties || {}).length === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
toolName,
|
|
264
|
+
description,
|
|
265
|
+
inputSchema,
|
|
266
|
+
outputSchema: this.getMcpOutputSchema(),
|
|
267
|
+
location: {
|
|
268
|
+
file: filePath,
|
|
269
|
+
line: lineNumber,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Infer registry pattern from object shape
|
|
275
|
+
* Checks if object entries look like tool definitions
|
|
276
|
+
*/
|
|
277
|
+
inferRegistryFromShape(obj, varName, filePath) {
|
|
278
|
+
const schemas = [];
|
|
279
|
+
const props = obj.getProperties();
|
|
280
|
+
// Must have at least one property
|
|
281
|
+
if (props.length === 0)
|
|
282
|
+
return schemas;
|
|
283
|
+
// Check first entry to see if it looks like a tool definition
|
|
284
|
+
const firstProp = props[0];
|
|
285
|
+
if (!Node.isPropertyAssignment(firstProp))
|
|
286
|
+
return schemas;
|
|
287
|
+
const firstInit = firstProp.getInitializer();
|
|
288
|
+
if (!firstInit || !Node.isObjectLiteralExpression(firstInit))
|
|
289
|
+
return schemas;
|
|
290
|
+
// Check if this looks like a ToolDefinition
|
|
291
|
+
const firstShape = this.detectToolDefinitionShape(firstInit);
|
|
292
|
+
if (!firstShape)
|
|
293
|
+
return schemas;
|
|
294
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
295
|
+
console.error(`[TypeScript] Detected tool definition shape in ${varName}: ${JSON.stringify(firstShape)}`);
|
|
296
|
+
}
|
|
297
|
+
// Parse all entries using detected shape
|
|
298
|
+
for (const prop of props) {
|
|
299
|
+
if (!Node.isPropertyAssignment(prop))
|
|
300
|
+
continue;
|
|
301
|
+
const entryName = prop.getName();
|
|
302
|
+
const entryValue = prop.getInitializer();
|
|
303
|
+
if (!entryValue || !Node.isObjectLiteralExpression(entryValue))
|
|
304
|
+
continue;
|
|
305
|
+
const toolDef = this.parseToolDefinition(entryValue, firstShape, filePath, prop.getStartLineNumber());
|
|
306
|
+
if (toolDef) {
|
|
307
|
+
if (toolDef.toolName === 'unknown') {
|
|
308
|
+
toolDef.toolName = entryName;
|
|
309
|
+
}
|
|
310
|
+
schemas.push(toolDef);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return schemas;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Detect the shape of a tool definition from an object literal
|
|
317
|
+
*/
|
|
318
|
+
detectToolDefinitionShape(obj) {
|
|
319
|
+
const propNames = new Set();
|
|
320
|
+
for (const prop of obj.getProperties()) {
|
|
321
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
322
|
+
propNames.add(prop.getName());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Must have name, description, and some kind of schema/handler
|
|
326
|
+
const hasName = propNames.has('name') || propNames.has('toolName');
|
|
327
|
+
const hasDescription = propNames.has('description');
|
|
328
|
+
const hasSchema = propNames.has('inputSchema') || propNames.has('schema') || propNames.has('parameters');
|
|
329
|
+
const hasHandler = propNames.has('handler') || propNames.has('execute') || propNames.has('run');
|
|
330
|
+
// Need at least name and (schema or handler)
|
|
331
|
+
if (!hasName && !(hasSchema || hasHandler)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
nameField: propNames.has('name') ? 'name' : 'toolName',
|
|
336
|
+
descriptionField: 'description',
|
|
337
|
+
schemaField: propNames.has('inputSchema') ? 'inputSchema' : (propNames.has('schema') ? 'schema' : 'parameters'),
|
|
338
|
+
handlerField: propNames.has('handler') ? 'handler' : (propNames.has('execute') ? 'execute' : 'run'),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Extract input schema from various formats
|
|
343
|
+
*/
|
|
344
|
+
extractInputSchema(node) {
|
|
345
|
+
const text = node.getText();
|
|
346
|
+
// Case 1: Direct object literal (already JSON Schema)
|
|
347
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
348
|
+
return this.parseJsonSchemaLiteral(node);
|
|
349
|
+
}
|
|
350
|
+
// Case 2: toJsonSchema(zodSchema) call
|
|
351
|
+
if (Node.isCallExpression(node)) {
|
|
352
|
+
const expr = node.getExpression();
|
|
353
|
+
const exprText = expr.getText();
|
|
354
|
+
// toJsonSchema(createCharacterSchema) pattern
|
|
355
|
+
if (exprText === 'toJsonSchema' || exprText.endsWith('.toJsonSchema')) {
|
|
356
|
+
const args = node.getArguments();
|
|
357
|
+
if (args.length > 0) {
|
|
358
|
+
const schemaRef = args[0].getText();
|
|
359
|
+
// Look up in cache
|
|
360
|
+
const cached = this.zodSchemaCache.get(schemaRef);
|
|
361
|
+
if (cached) {
|
|
362
|
+
return cached.schema;
|
|
363
|
+
}
|
|
364
|
+
// Return reference for later resolution
|
|
365
|
+
return { $ref: `#/definitions/${schemaRef}` };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Case 3: Identifier referencing a Zod schema
|
|
370
|
+
if (Node.isIdentifier(node)) {
|
|
371
|
+
const schemaName = node.getText();
|
|
372
|
+
const cached = this.zodSchemaCache.get(schemaName);
|
|
373
|
+
if (cached) {
|
|
374
|
+
return cached.schema;
|
|
375
|
+
}
|
|
376
|
+
// Check if it's a *Schema naming convention
|
|
377
|
+
if (schemaName.endsWith('Schema')) {
|
|
378
|
+
return { $ref: `#/definitions/${schemaName}` };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Case 4: Inline Zod schema (z.object(...))
|
|
382
|
+
if (text.startsWith('z.object(') || text.includes('z.object(')) {
|
|
383
|
+
return this.parseZodSchema(node);
|
|
384
|
+
}
|
|
385
|
+
return { type: 'object' };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Parse a JSON Schema object literal
|
|
389
|
+
*/
|
|
390
|
+
parseJsonSchemaLiteral(obj) {
|
|
391
|
+
const schema = {};
|
|
392
|
+
for (const prop of obj.getProperties()) {
|
|
393
|
+
if (!Node.isPropertyAssignment(prop))
|
|
394
|
+
continue;
|
|
395
|
+
const propName = prop.getName();
|
|
396
|
+
const init = prop.getInitializer();
|
|
397
|
+
if (!init)
|
|
398
|
+
continue;
|
|
399
|
+
switch (propName) {
|
|
400
|
+
case 'type':
|
|
401
|
+
if (Node.isStringLiteral(init)) {
|
|
402
|
+
schema.type = init.getLiteralValue();
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
case 'properties':
|
|
406
|
+
if (Node.isObjectLiteralExpression(init)) {
|
|
407
|
+
schema.properties = {};
|
|
408
|
+
for (const subProp of init.getProperties()) {
|
|
409
|
+
if (Node.isPropertyAssignment(subProp)) {
|
|
410
|
+
const subName = subProp.getName();
|
|
411
|
+
const subInit = subProp.getInitializer();
|
|
412
|
+
if (subInit && Node.isObjectLiteralExpression(subInit)) {
|
|
413
|
+
schema.properties[subName] = this.parseJsonSchemaLiteral(subInit);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
case 'required':
|
|
420
|
+
if (Node.isArrayLiteralExpression(init)) {
|
|
421
|
+
schema.required = init.getElements()
|
|
422
|
+
.filter(e => Node.isStringLiteral(e))
|
|
423
|
+
.map(e => e.getLiteralValue());
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
case 'description':
|
|
427
|
+
if (Node.isStringLiteral(init)) {
|
|
428
|
+
schema.description = init.getLiteralValue();
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return schema;
|
|
434
|
+
}
|
|
435
|
+
// ==========================================================================
|
|
436
|
+
// Method 3: Exported Zod Schemas
|
|
437
|
+
// ==========================================================================
|
|
438
|
+
/**
|
|
439
|
+
* Collect all exported Zod schemas for cross-reference
|
|
440
|
+
*/
|
|
441
|
+
collectZodSchemas(sourceFile, filePath) {
|
|
442
|
+
sourceFile.forEachDescendant((node) => {
|
|
443
|
+
if (Node.isVariableDeclaration(node)) {
|
|
444
|
+
const varName = node.getName();
|
|
445
|
+
// Look for *Schema naming convention
|
|
446
|
+
if (varName.endsWith('Schema')) {
|
|
447
|
+
const init = node.getInitializer();
|
|
448
|
+
if (init) {
|
|
449
|
+
const text = init.getText();
|
|
450
|
+
// Check if it's a Zod schema definition
|
|
451
|
+
if (text.includes('z.object(') || text.includes('z.union(') ||
|
|
452
|
+
text.includes('z.string()') || text.includes('z.number()')) {
|
|
453
|
+
const parsedSchema = this.parseZodSchema(init);
|
|
454
|
+
this.zodSchemaCache.set(varName, {
|
|
455
|
+
schema: parsedSchema,
|
|
456
|
+
location: {
|
|
457
|
+
file: filePath,
|
|
458
|
+
line: node.getStartLineNumber(),
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Extract tool definitions from exports
|
|
469
|
+
*/
|
|
470
|
+
extractFromExports(sourceFile, filePath) {
|
|
471
|
+
const schemas = [];
|
|
472
|
+
// Look for exported functions that match tool handler signatures
|
|
473
|
+
for (const exportDecl of sourceFile.getExportedDeclarations()) {
|
|
474
|
+
const [name, declarations] = exportDecl;
|
|
475
|
+
for (const decl of declarations) {
|
|
476
|
+
// Check for exported async functions with matching schema
|
|
477
|
+
if (Node.isFunctionDeclaration(decl) && decl.isAsync()) {
|
|
478
|
+
const schemaName = `${name}Schema`;
|
|
479
|
+
const cached = this.zodSchemaCache.get(schemaName);
|
|
480
|
+
if (cached) {
|
|
481
|
+
// Get JSDoc description if available
|
|
482
|
+
const jsDocs = decl.getJsDocs();
|
|
483
|
+
const description = jsDocs.length > 0
|
|
484
|
+
? jsDocs[0].getDescription().trim()
|
|
485
|
+
: undefined;
|
|
486
|
+
schemas.push({
|
|
487
|
+
toolName: name,
|
|
488
|
+
description,
|
|
489
|
+
inputSchema: cached.schema,
|
|
490
|
+
outputSchema: this.getMcpOutputSchema(),
|
|
491
|
+
location: {
|
|
492
|
+
file: filePath,
|
|
493
|
+
line: decl.getStartLineNumber(),
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return schemas;
|
|
501
|
+
}
|
|
502
|
+
// ==========================================================================
|
|
503
|
+
// Zod Schema Parsing (Enhanced)
|
|
504
|
+
// ==========================================================================
|
|
505
|
+
parseZodSchema(node) {
|
|
506
|
+
const schema = {
|
|
507
|
+
type: 'object',
|
|
508
|
+
properties: {},
|
|
509
|
+
required: [],
|
|
510
|
+
};
|
|
511
|
+
// Handle z.object({...})
|
|
512
|
+
if (Node.isCallExpression(node)) {
|
|
513
|
+
const expr = node.getExpression();
|
|
514
|
+
const exprText = expr.getText();
|
|
515
|
+
if (exprText === 'z.object' || exprText.endsWith('.object')) {
|
|
516
|
+
const args = node.getArguments();
|
|
517
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
|
|
518
|
+
return this.parseZodObjectContent(args[0]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Handle z.union([...])
|
|
522
|
+
if (exprText === 'z.union' || exprText.endsWith('.union')) {
|
|
523
|
+
return this.parseZodUnion(node);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Handle direct object literal (for older Zod style)
|
|
527
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
528
|
+
return this.parseZodObjectContent(node);
|
|
529
|
+
}
|
|
530
|
+
return schema;
|
|
531
|
+
}
|
|
532
|
+
parseZodObjectContent(obj) {
|
|
533
|
+
const schema = {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {},
|
|
536
|
+
required: [],
|
|
537
|
+
};
|
|
538
|
+
for (const prop of obj.getProperties()) {
|
|
539
|
+
if (!Node.isPropertyAssignment(prop))
|
|
540
|
+
continue;
|
|
541
|
+
const propName = prop.getName();
|
|
542
|
+
const initializer = prop.getInitializer();
|
|
543
|
+
if (!initializer)
|
|
544
|
+
continue;
|
|
545
|
+
const propSchema = this.parseZodType(initializer);
|
|
546
|
+
schema.properties[propName] = propSchema;
|
|
547
|
+
const initText = initializer.getText();
|
|
548
|
+
if (!initText.includes('.optional()') && !initText.includes('.nullish()')) {
|
|
549
|
+
schema.required.push(propName);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return schema;
|
|
553
|
+
}
|
|
554
|
+
parseZodUnion(node) {
|
|
555
|
+
const args = node.getArguments();
|
|
556
|
+
if (args.length === 0)
|
|
557
|
+
return { type: 'object' };
|
|
558
|
+
const firstArg = args[0];
|
|
559
|
+
if (!Node.isArrayLiteralExpression(firstArg))
|
|
560
|
+
return { type: 'object' };
|
|
561
|
+
const variants = [];
|
|
562
|
+
for (const element of firstArg.getElements()) {
|
|
563
|
+
variants.push(this.parseZodType(element));
|
|
564
|
+
}
|
|
565
|
+
if (variants.length === 1) {
|
|
566
|
+
return variants[0];
|
|
567
|
+
}
|
|
568
|
+
return { anyOf: variants };
|
|
569
|
+
}
|
|
570
|
+
parseZodType(node) {
|
|
571
|
+
const text = node.getText();
|
|
572
|
+
// z.string()
|
|
573
|
+
if (text.startsWith('z.string()') || text.match(/^z\.string\(\)/)) {
|
|
574
|
+
const schema = { type: 'string' };
|
|
575
|
+
// Extract description
|
|
576
|
+
const descMatch = text.match(/\.describe\(['"](.+?)['"]\)/);
|
|
577
|
+
if (descMatch) {
|
|
578
|
+
schema.description = descMatch[1];
|
|
579
|
+
}
|
|
580
|
+
// Extract min/max
|
|
581
|
+
const minMatch = text.match(/\.min\((\d+)\)/);
|
|
582
|
+
const maxMatch = text.match(/\.max\((\d+)\)/);
|
|
583
|
+
if (minMatch)
|
|
584
|
+
schema.minLength = parseInt(minMatch[1]);
|
|
585
|
+
if (maxMatch)
|
|
586
|
+
schema.maxLength = parseInt(maxMatch[1]);
|
|
587
|
+
return schema;
|
|
588
|
+
}
|
|
589
|
+
// z.number()
|
|
590
|
+
if (text.startsWith('z.number()') || text.match(/^z\.number\(\)/)) {
|
|
591
|
+
const schema = { type: 'number' };
|
|
592
|
+
const minMatch = text.match(/\.min\((-?\d+)\)/);
|
|
593
|
+
const maxMatch = text.match(/\.max\((-?\d+)\)/);
|
|
594
|
+
if (minMatch)
|
|
595
|
+
schema.minimum = parseInt(minMatch[1]);
|
|
596
|
+
if (maxMatch)
|
|
597
|
+
schema.maximum = parseInt(maxMatch[1]);
|
|
598
|
+
return schema;
|
|
599
|
+
}
|
|
600
|
+
// z.boolean()
|
|
601
|
+
if (text.startsWith('z.boolean()')) {
|
|
602
|
+
return { type: 'boolean' };
|
|
603
|
+
}
|
|
604
|
+
// z.enum([...])
|
|
605
|
+
if (text.includes('z.enum(')) {
|
|
606
|
+
const enumMatch = text.match(/z\.enum\(\[(.*?)\]\)/s);
|
|
607
|
+
if (enumMatch) {
|
|
608
|
+
const values = enumMatch[1]
|
|
609
|
+
.split(',')
|
|
610
|
+
.map(v => v.trim().replace(/['"]/g, ''))
|
|
611
|
+
.filter(v => v.length > 0);
|
|
612
|
+
return { type: 'string', enum: values };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// z.array(...)
|
|
616
|
+
if (text.startsWith('z.array(')) {
|
|
617
|
+
return { type: 'array' };
|
|
618
|
+
}
|
|
619
|
+
// z.object(...)
|
|
620
|
+
if (text.startsWith('z.object(')) {
|
|
621
|
+
if (Node.isCallExpression(node)) {
|
|
622
|
+
return this.parseZodSchema(node);
|
|
623
|
+
}
|
|
624
|
+
return { type: 'object' };
|
|
625
|
+
}
|
|
626
|
+
// z.literal(...)
|
|
627
|
+
if (text.includes('z.literal(')) {
|
|
628
|
+
const match = text.match(/z\.literal\(['"](.+?)['"]\)/);
|
|
629
|
+
if (match) {
|
|
630
|
+
return { type: 'string', const: match[1] };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Reference to another schema
|
|
634
|
+
if (text.endsWith('Schema') || this.zodSchemaCache.has(text)) {
|
|
635
|
+
const cached = this.zodSchemaCache.get(text);
|
|
636
|
+
if (cached) {
|
|
637
|
+
return cached.schema;
|
|
638
|
+
}
|
|
639
|
+
return { $ref: `#/definitions/${text}` };
|
|
640
|
+
}
|
|
641
|
+
return { type: 'unknown' };
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Standard MCP tool output schema
|
|
645
|
+
*/
|
|
646
|
+
getMcpOutputSchema() {
|
|
647
|
+
return {
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {
|
|
650
|
+
content: {
|
|
651
|
+
type: 'array',
|
|
652
|
+
items: {
|
|
653
|
+
type: 'object',
|
|
654
|
+
properties: {
|
|
655
|
+
type: { type: 'string' },
|
|
656
|
+
text: { type: 'string' },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
// ==========================================================================
|
|
664
|
+
// Consumer Usage Tracing
|
|
665
|
+
// ==========================================================================
|
|
666
|
+
async traceUsage(options) {
|
|
667
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
668
|
+
console.error(`[TypeScript] Tracing: ${options.rootDir}`);
|
|
669
|
+
}
|
|
670
|
+
const project = new Project({
|
|
671
|
+
skipAddingFilesFromTsConfig: true,
|
|
672
|
+
});
|
|
673
|
+
const patterns = options.include || this.filePatterns;
|
|
674
|
+
const excludePatterns = options.exclude || ['**/node_modules/**', '**/dist/**'];
|
|
675
|
+
project.addSourceFilesAtPaths(patterns.map(p => `${options.rootDir}/${p}`));
|
|
676
|
+
const schemas = [];
|
|
677
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
678
|
+
const filePath = sourceFile.getFilePath();
|
|
679
|
+
if (excludePatterns.some(pattern => filePath.includes(pattern.replace('**/', '')))) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const fileSchemas = this.traceFile(sourceFile, filePath);
|
|
683
|
+
schemas.push(...fileSchemas);
|
|
684
|
+
}
|
|
685
|
+
if (process.env.DEBUG_TRACE_MCP) {
|
|
686
|
+
console.error(`[TypeScript] Found ${schemas.length} tool calls`);
|
|
687
|
+
}
|
|
688
|
+
return schemas;
|
|
689
|
+
}
|
|
690
|
+
traceFile(sourceFile, filePath) {
|
|
691
|
+
const schemas = [];
|
|
692
|
+
sourceFile.forEachDescendant((node) => {
|
|
693
|
+
if (Node.isCallExpression(node)) {
|
|
694
|
+
const callInfo = this.parseCallToolExpression(node);
|
|
695
|
+
if (callInfo) {
|
|
696
|
+
const expectedProps = this.traceResultUsage(node);
|
|
697
|
+
schemas.push({
|
|
698
|
+
toolName: callInfo.toolName,
|
|
699
|
+
callSite: {
|
|
700
|
+
file: filePath,
|
|
701
|
+
line: node.getStartLineNumber(),
|
|
702
|
+
column: node.getStartLinePos(),
|
|
703
|
+
},
|
|
704
|
+
argumentsProvided: callInfo.arguments,
|
|
705
|
+
expectedProperties: expectedProps,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
return schemas;
|
|
711
|
+
}
|
|
712
|
+
parseCallToolExpression(node) {
|
|
713
|
+
const expression = node.getExpression();
|
|
714
|
+
if (!Node.isPropertyAccessExpression(expression)) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const methodName = expression.getName();
|
|
718
|
+
// Support multiple call patterns
|
|
719
|
+
const callPatterns = ['callTool', 'call', 'invoke', 'execute'];
|
|
720
|
+
if (!callPatterns.includes(methodName)) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const args = node.getArguments();
|
|
724
|
+
if (args.length < 1) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
// Tool name could be first arg or in an options object
|
|
728
|
+
let toolName = 'unknown';
|
|
729
|
+
let providedArgs = {};
|
|
730
|
+
const firstArg = args[0];
|
|
731
|
+
// Pattern 1: callTool('toolName', { args })
|
|
732
|
+
if (Node.isStringLiteral(firstArg)) {
|
|
733
|
+
toolName = firstArg.getLiteralValue();
|
|
734
|
+
if (args.length > 1) {
|
|
735
|
+
providedArgs = this.parseObjectLiteral(args[1]);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Pattern 2: callTool({ name: 'toolName', arguments: {...} })
|
|
739
|
+
else if (Node.isObjectLiteralExpression(firstArg)) {
|
|
740
|
+
for (const prop of firstArg.getProperties()) {
|
|
741
|
+
if (!Node.isPropertyAssignment(prop))
|
|
742
|
+
continue;
|
|
743
|
+
const propName = prop.getName();
|
|
744
|
+
const init = prop.getInitializer();
|
|
745
|
+
if (propName === 'name' && init && Node.isStringLiteral(init)) {
|
|
746
|
+
toolName = init.getLiteralValue();
|
|
747
|
+
}
|
|
748
|
+
if ((propName === 'arguments' || propName === 'args') && init) {
|
|
749
|
+
providedArgs = this.parseObjectLiteral(init);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return { toolName, arguments: providedArgs };
|
|
754
|
+
}
|
|
755
|
+
parseObjectLiteral(node) {
|
|
756
|
+
const result = {};
|
|
757
|
+
if (!Node.isObjectLiteralExpression(node)) {
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
for (const prop of node.getProperties()) {
|
|
761
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
762
|
+
const name = prop.getName();
|
|
763
|
+
result[name] = '<value>';
|
|
764
|
+
}
|
|
765
|
+
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
766
|
+
const name = prop.getName();
|
|
767
|
+
result[name] = '<value>';
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
traceResultUsage(callNode) {
|
|
773
|
+
const properties = new Set();
|
|
774
|
+
const containingFunction = callNode.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration)
|
|
775
|
+
|| callNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction)
|
|
776
|
+
|| callNode.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
777
|
+
if (!containingFunction) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
containingFunction.forEachDescendant((node) => {
|
|
781
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
782
|
+
const propChain = this.getPropertyChain(node);
|
|
783
|
+
const mcpBoilerplate = ['content', 'text', 'type', 'length'];
|
|
784
|
+
const meaningfulProps = propChain.filter(p => !mcpBoilerplate.includes(p));
|
|
785
|
+
for (const prop of meaningfulProps) {
|
|
786
|
+
properties.add(prop);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return Array.from(properties);
|
|
791
|
+
}
|
|
792
|
+
getPropertyChain(node) {
|
|
793
|
+
const chain = [];
|
|
794
|
+
let current = node;
|
|
795
|
+
while (current && Node.isPropertyAccessExpression(current)) {
|
|
796
|
+
chain.unshift(current.getName());
|
|
797
|
+
current = current.getExpression();
|
|
798
|
+
}
|
|
799
|
+
return chain;
|
|
800
|
+
}
|
|
801
|
+
// ==========================================================================
|
|
802
|
+
// 📋 Interface, Type Alias, and Enum Extraction
|
|
803
|
+
// ==========================================================================
|
|
804
|
+
/**
|
|
805
|
+
* Extract interfaces, type aliases, and enums from TypeScript files.
|
|
806
|
+
*
|
|
807
|
+
* Scans TypeScript source files and extracts all exported type definitions,
|
|
808
|
+
* converting them to NormalizedSchema format for schema comparison.
|
|
809
|
+
*
|
|
810
|
+
* Supports extraction of:
|
|
811
|
+
* - **Interfaces** - Including inherited properties from extends
|
|
812
|
+
* - **Type Aliases** - Object types, unions, intersections, Records
|
|
813
|
+
* - **Enums** - Both const and regular enums
|
|
814
|
+
*
|
|
815
|
+
* @param options - Extraction options specifying root directory and file patterns
|
|
816
|
+
* @param options.rootDir - Root directory to search for TypeScript files
|
|
817
|
+
* @param options.include - Optional glob patterns to include (default: `['*.ts', '*.tsx']`)
|
|
818
|
+
* @param options.exclude - Optional glob patterns to exclude (default: excludes node_modules and dist)
|
|
819
|
+
* @returns Promise resolving to array of NormalizedSchema for all exported declarations
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* const parser = new TypeScriptParser();
|
|
824
|
+
* const schemas = await parser.extractInterfaces({
|
|
825
|
+
* rootDir: './src/types',
|
|
826
|
+
* include: ['*.ts'],
|
|
827
|
+
* exclude: ['*.test.ts']
|
|
828
|
+
* });
|
|
829
|
+
*
|
|
830
|
+
* // schemas[0] = {
|
|
831
|
+
* // name: 'UserProfile',
|
|
832
|
+
* // properties: { id: {...}, name: {...} },
|
|
833
|
+
* // required: ['id', 'name'],
|
|
834
|
+
* // source: { source: 'typescript', id: 'interface:UserProfile@...' },
|
|
835
|
+
* // location: { file: '...', line: 10 }
|
|
836
|
+
* // }
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
async extractInterfaces(options) {
|
|
840
|
+
const project = new Project({
|
|
841
|
+
tsConfigFilePath: undefined,
|
|
842
|
+
skipAddingFilesFromTsConfig: true,
|
|
843
|
+
compilerOptions: {
|
|
844
|
+
strict: true,
|
|
845
|
+
target: ts.ScriptTarget.ESNext,
|
|
846
|
+
module: ts.ModuleKind.ESNext,
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
const patterns = options.include || this.filePatterns;
|
|
850
|
+
const excludePatterns = options.exclude || ['**/node_modules/**', '**/dist/**'];
|
|
851
|
+
project.addSourceFilesAtPaths(patterns.map(p => `${options.rootDir}/${p}`));
|
|
852
|
+
const schemas = [];
|
|
853
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
854
|
+
const filePath = sourceFile.getFilePath();
|
|
855
|
+
if (excludePatterns.some(pattern => filePath.includes(pattern.replace('**/', '')))) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
// Extract interfaces
|
|
859
|
+
for (const iface of sourceFile.getInterfaces()) {
|
|
860
|
+
if (!iface.isExported())
|
|
861
|
+
continue;
|
|
862
|
+
const schema = this.convertInterfaceToSchema(iface, filePath);
|
|
863
|
+
schemas.push(schema);
|
|
864
|
+
}
|
|
865
|
+
// Extract type aliases
|
|
866
|
+
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
867
|
+
if (!typeAlias.isExported())
|
|
868
|
+
continue;
|
|
869
|
+
const schema = this.convertTypeAliasToSchema(typeAlias, filePath);
|
|
870
|
+
schemas.push(schema);
|
|
871
|
+
}
|
|
872
|
+
// Extract enums
|
|
873
|
+
for (const enumDecl of sourceFile.getEnums()) {
|
|
874
|
+
if (!enumDecl.isExported())
|
|
875
|
+
continue;
|
|
876
|
+
const schema = this.convertEnumToSchema(enumDecl, filePath);
|
|
877
|
+
schemas.push(schema);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return schemas;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Extract all schemas from TypeScript files.
|
|
884
|
+
*
|
|
885
|
+
* Comprehensive extraction that combines interface/type extraction.
|
|
886
|
+
* This is the recommended entry point for extracting TypeScript type
|
|
887
|
+
* definitions for schema comparison workflows.
|
|
888
|
+
*
|
|
889
|
+
* @param options - Extraction options specifying root directory and file patterns
|
|
890
|
+
* @param options.rootDir - Root directory to search for TypeScript files
|
|
891
|
+
* @param options.include - Optional glob patterns to include
|
|
892
|
+
* @param options.exclude - Optional glob patterns to exclude
|
|
893
|
+
* @returns Promise resolving to array of NormalizedSchema
|
|
894
|
+
*
|
|
895
|
+
* @remarks
|
|
896
|
+
* Currently returns interface/type/enum schemas only.
|
|
897
|
+
* Future enhancement: Convert ProducerSchema to NormalizedSchema
|
|
898
|
+
* for Zod schema inclusion.
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* ```typescript
|
|
902
|
+
* const parser = new TypeScriptParser();
|
|
903
|
+
* const schemas = await parser.extractAll({ rootDir: './src' });
|
|
904
|
+
*
|
|
905
|
+
* // Returns all exported interfaces, type aliases, and enums
|
|
906
|
+
* console.log(`Found ${schemas.length} type definitions`);
|
|
907
|
+
* ```
|
|
908
|
+
*/
|
|
909
|
+
async extractAll(options) {
|
|
910
|
+
// Extract interfaces, type aliases, and enums
|
|
911
|
+
const interfaceSchemas = await this.extractInterfaces(options);
|
|
912
|
+
// Note: extractSchemas returns ProducerSchema[], not NormalizedSchema[]
|
|
913
|
+
// For now, we return only interface schemas.
|
|
914
|
+
// A future enhancement could convert ProducerSchema to NormalizedSchema.
|
|
915
|
+
return interfaceSchemas;
|
|
916
|
+
}
|
|
917
|
+
// --------------------------------------------------------------------------
|
|
918
|
+
// Private: Interface Conversion
|
|
919
|
+
// --------------------------------------------------------------------------
|
|
920
|
+
/**
|
|
921
|
+
* Convert a TypeScript interface declaration to NormalizedSchema.
|
|
922
|
+
*
|
|
923
|
+
* Handles all interface features:
|
|
924
|
+
* - Direct properties with type annotations
|
|
925
|
+
* - Inherited properties from extends clauses
|
|
926
|
+
* - Optional properties (marked with ?)
|
|
927
|
+
* - Readonly properties
|
|
928
|
+
* - Nullable types (T | null)
|
|
929
|
+
* - JSDoc descriptions and @deprecated tags
|
|
930
|
+
*
|
|
931
|
+
* @param iface - The ts-morph InterfaceDeclaration to convert
|
|
932
|
+
* @param filePath - Path to the source file for location tracking
|
|
933
|
+
* @returns NormalizedSchema representation of the interface
|
|
934
|
+
*/
|
|
935
|
+
convertInterfaceToSchema(iface, filePath) {
|
|
936
|
+
const name = iface.getName();
|
|
937
|
+
const properties = {};
|
|
938
|
+
const required = [];
|
|
939
|
+
// Get all properties including inherited ones
|
|
940
|
+
const allProperties = this.getInterfaceProperties(iface);
|
|
941
|
+
for (const prop of allProperties) {
|
|
942
|
+
const propName = prop.getName();
|
|
943
|
+
const propDecl = prop.getDeclarations()[0];
|
|
944
|
+
// Get the type from the value declaration or the declared type
|
|
945
|
+
const propType = prop.getValueDeclaration()
|
|
946
|
+
? prop.getValueDeclarationOrThrow().getType()
|
|
947
|
+
: prop.getDeclaredType();
|
|
948
|
+
const isOptional = prop.isOptional();
|
|
949
|
+
const isReadonly = propDecl ? this.isPropertyReadonly(propDecl) : false;
|
|
950
|
+
const jsDocInfo = propDecl ? this.getJSDocInfo(propDecl) : { description: undefined, deprecated: false };
|
|
951
|
+
// Check for nullable types
|
|
952
|
+
const { baseType, isNullable, hasUndefined } = this.analyzeNullability(propType);
|
|
953
|
+
// Start with interface name in visitingTypes to prevent infinite recursion
|
|
954
|
+
const visitingTypes = new Set([name]);
|
|
955
|
+
const normalizedType = this.convertTypeToNormalized(baseType, visitingTypes);
|
|
956
|
+
properties[propName] = {
|
|
957
|
+
type: normalizedType,
|
|
958
|
+
optional: isOptional || hasUndefined,
|
|
959
|
+
nullable: isNullable,
|
|
960
|
+
readonly: isReadonly,
|
|
961
|
+
deprecated: jsDocInfo.deprecated,
|
|
962
|
+
description: jsDocInfo.description,
|
|
963
|
+
};
|
|
964
|
+
if (!isOptional && !hasUndefined) {
|
|
965
|
+
required.push(propName);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const schemaRef = {
|
|
969
|
+
source: 'typescript',
|
|
970
|
+
id: `interface:${name}@${filePath}`,
|
|
971
|
+
};
|
|
972
|
+
return {
|
|
973
|
+
name,
|
|
974
|
+
properties,
|
|
975
|
+
required,
|
|
976
|
+
source: schemaRef,
|
|
977
|
+
location: {
|
|
978
|
+
file: filePath,
|
|
979
|
+
line: iface.getStartLineNumber(),
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get all properties from an interface, including inherited ones.
|
|
985
|
+
*
|
|
986
|
+
* Uses ts-morph's type resolution to flatten the property list,
|
|
987
|
+
* ensuring all properties from extended interfaces are included.
|
|
988
|
+
*
|
|
989
|
+
* @param iface - The interface declaration
|
|
990
|
+
* @returns Array of ts-morph Symbol objects representing all properties
|
|
991
|
+
*/
|
|
992
|
+
getInterfaceProperties(iface) {
|
|
993
|
+
const type = iface.getType();
|
|
994
|
+
return type.getProperties();
|
|
995
|
+
}
|
|
996
|
+
// --------------------------------------------------------------------------
|
|
997
|
+
// Private: Type Alias Conversion
|
|
998
|
+
// --------------------------------------------------------------------------
|
|
999
|
+
/**
|
|
1000
|
+
* Convert a TypeScript type alias to NormalizedSchema.
|
|
1001
|
+
*
|
|
1002
|
+
* Handles various type alias patterns:
|
|
1003
|
+
* - Object types: `type Foo = { bar: string }`
|
|
1004
|
+
* - Intersection types: `type Foo = A & B`
|
|
1005
|
+
* - Record types: `type Dict = Record<string, T>`
|
|
1006
|
+
* - Union types: `type Status = 'active' | 'inactive'`
|
|
1007
|
+
*
|
|
1008
|
+
* @param typeAlias - The ts-morph TypeAliasDeclaration to convert
|
|
1009
|
+
* @param filePath - Path to the source file for location tracking
|
|
1010
|
+
* @returns NormalizedSchema representation of the type alias
|
|
1011
|
+
*/
|
|
1012
|
+
convertTypeAliasToSchema(typeAlias, filePath) {
|
|
1013
|
+
const name = typeAlias.getName();
|
|
1014
|
+
const aliasType = typeAlias.getType();
|
|
1015
|
+
// Handle object-like type aliases (type Foo = { ... })
|
|
1016
|
+
const properties = {};
|
|
1017
|
+
const required = [];
|
|
1018
|
+
let additionalProperties;
|
|
1019
|
+
// Check if it's a Record type (has index signature)
|
|
1020
|
+
const stringIndexType = aliasType.getStringIndexType();
|
|
1021
|
+
if (stringIndexType) {
|
|
1022
|
+
// This is a Record<string, T> or similar
|
|
1023
|
+
additionalProperties = this.convertTypeToNormalized(stringIndexType);
|
|
1024
|
+
}
|
|
1025
|
+
// Check if it's an intersection type and flatten it
|
|
1026
|
+
if (aliasType.isIntersection()) {
|
|
1027
|
+
const intersectionTypes = aliasType.getIntersectionTypes();
|
|
1028
|
+
for (const t of intersectionTypes) {
|
|
1029
|
+
this.extractPropertiesFromType(t, properties, required);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
else if (aliasType.isObject()) {
|
|
1033
|
+
this.extractPropertiesFromType(aliasType, properties, required);
|
|
1034
|
+
}
|
|
1035
|
+
// For union types (like string literals), still return a schema
|
|
1036
|
+
const schemaRef = {
|
|
1037
|
+
source: 'typescript',
|
|
1038
|
+
id: `type:${name}@${filePath}`,
|
|
1039
|
+
};
|
|
1040
|
+
const result = {
|
|
1041
|
+
name,
|
|
1042
|
+
properties,
|
|
1043
|
+
required,
|
|
1044
|
+
source: schemaRef,
|
|
1045
|
+
location: {
|
|
1046
|
+
file: filePath,
|
|
1047
|
+
line: typeAlias.getStartLineNumber(),
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
if (additionalProperties !== undefined) {
|
|
1051
|
+
result.additionalProperties = additionalProperties;
|
|
1052
|
+
}
|
|
1053
|
+
return result;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Extract properties from a ts-morph Type object.
|
|
1057
|
+
*
|
|
1058
|
+
* Helper method for extracting property definitions from resolved types.
|
|
1059
|
+
* Used by both interface and type alias conversion to handle object types.
|
|
1060
|
+
*
|
|
1061
|
+
* @param type - The ts-morph Type to extract properties from
|
|
1062
|
+
* @param properties - Output record to populate with PropertyDef entries
|
|
1063
|
+
* @param required - Output array to populate with required property names
|
|
1064
|
+
*/
|
|
1065
|
+
extractPropertiesFromType(type, properties, required, visitingTypes = new Set()) {
|
|
1066
|
+
for (const prop of type.getProperties()) {
|
|
1067
|
+
const propName = prop.getName();
|
|
1068
|
+
const propDecl = prop.getDeclarations()[0];
|
|
1069
|
+
const propType = prop.getValueDeclaration()
|
|
1070
|
+
? prop.getValueDeclarationOrThrow().getType()
|
|
1071
|
+
: prop.getDeclaredType();
|
|
1072
|
+
const isOptional = prop.isOptional();
|
|
1073
|
+
const isReadonly = propDecl ? this.isPropertyReadonly(propDecl) : false;
|
|
1074
|
+
const jsDocInfo = propDecl ? this.getJSDocInfo(propDecl) : { description: undefined, deprecated: false };
|
|
1075
|
+
const { baseType, isNullable, hasUndefined } = this.analyzeNullability(propType);
|
|
1076
|
+
const normalizedType = this.convertTypeToNormalized(baseType, visitingTypes);
|
|
1077
|
+
properties[propName] = {
|
|
1078
|
+
type: normalizedType,
|
|
1079
|
+
optional: isOptional || hasUndefined,
|
|
1080
|
+
nullable: isNullable,
|
|
1081
|
+
readonly: isReadonly,
|
|
1082
|
+
deprecated: jsDocInfo.deprecated,
|
|
1083
|
+
description: jsDocInfo.description,
|
|
1084
|
+
};
|
|
1085
|
+
if (!isOptional && !hasUndefined && !properties[propName]) {
|
|
1086
|
+
required.push(propName);
|
|
1087
|
+
}
|
|
1088
|
+
else if (!isOptional && !hasUndefined && !required.includes(propName)) {
|
|
1089
|
+
required.push(propName);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// --------------------------------------------------------------------------
|
|
1094
|
+
// Private: Enum Conversion
|
|
1095
|
+
// --------------------------------------------------------------------------
|
|
1096
|
+
/**
|
|
1097
|
+
* Convert a TypeScript enum to NormalizedSchema.
|
|
1098
|
+
*
|
|
1099
|
+
* Enums are represented as schemas with empty properties,
|
|
1100
|
+
* since they don't have traditional object properties.
|
|
1101
|
+
* The enum values are not currently extracted to the schema.
|
|
1102
|
+
*
|
|
1103
|
+
* @param enumDecl - The ts-morph EnumDeclaration to convert
|
|
1104
|
+
* @param filePath - Path to the source file for location tracking
|
|
1105
|
+
* @returns NormalizedSchema representation of the enum
|
|
1106
|
+
*
|
|
1107
|
+
* @remarks
|
|
1108
|
+
* Future enhancement: Include enum members as values or properties.
|
|
1109
|
+
*/
|
|
1110
|
+
convertEnumToSchema(enumDecl, filePath) {
|
|
1111
|
+
const name = enumDecl.getName();
|
|
1112
|
+
const schemaRef = {
|
|
1113
|
+
source: 'typescript',
|
|
1114
|
+
id: `enum:${name}@${filePath}`,
|
|
1115
|
+
};
|
|
1116
|
+
// Enums don't have traditional properties, but we can represent them
|
|
1117
|
+
return {
|
|
1118
|
+
name,
|
|
1119
|
+
properties: {},
|
|
1120
|
+
required: [],
|
|
1121
|
+
source: schemaRef,
|
|
1122
|
+
location: {
|
|
1123
|
+
file: filePath,
|
|
1124
|
+
line: enumDecl.getStartLineNumber(),
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
// --------------------------------------------------------------------------
|
|
1129
|
+
// Private: Type Analysis Utilities
|
|
1130
|
+
// --------------------------------------------------------------------------
|
|
1131
|
+
/**
|
|
1132
|
+
* Analyze nullability of a TypeScript type.
|
|
1133
|
+
*
|
|
1134
|
+
* Examines union types to determine if they include null or undefined,
|
|
1135
|
+
* and extracts the "base" type after removing null/undefined.
|
|
1136
|
+
*
|
|
1137
|
+
* @param type - The ts-morph Type to analyze
|
|
1138
|
+
* @returns Object containing:
|
|
1139
|
+
* - `baseType`: The type with null/undefined removed
|
|
1140
|
+
* - `isNullable`: True if type includes `| null`
|
|
1141
|
+
* - `hasUndefined`: True if type includes `| undefined`
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* ```typescript
|
|
1145
|
+
* // For type: string | null | undefined
|
|
1146
|
+
* // Returns: { baseType: string, isNullable: true, hasUndefined: true }
|
|
1147
|
+
* ```
|
|
1148
|
+
*/
|
|
1149
|
+
analyzeNullability(type) {
|
|
1150
|
+
let isNullable = false;
|
|
1151
|
+
let hasUndefined = false;
|
|
1152
|
+
let baseType = type;
|
|
1153
|
+
if (type.isUnion()) {
|
|
1154
|
+
const unionTypes = type.getUnionTypes();
|
|
1155
|
+
const nonNullTypes = [];
|
|
1156
|
+
for (const t of unionTypes) {
|
|
1157
|
+
if (t.isNull()) {
|
|
1158
|
+
isNullable = true;
|
|
1159
|
+
}
|
|
1160
|
+
else if (t.isUndefined()) {
|
|
1161
|
+
hasUndefined = true;
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
nonNullTypes.push(t);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// If there's only one non-null type, use it as the base
|
|
1168
|
+
if (nonNullTypes.length === 1) {
|
|
1169
|
+
baseType = nonNullTypes[0];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return { baseType, isNullable, hasUndefined };
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Convert a ts-morph Type to NormalizedType.
|
|
1176
|
+
*
|
|
1177
|
+
* Recursively converts TypeScript types to the normalized type system:
|
|
1178
|
+
* - Primitives: string, number, boolean, null
|
|
1179
|
+
* - Literals: 'active', 42, true
|
|
1180
|
+
* - Arrays: `string[]`, `Array<T>`
|
|
1181
|
+
* - Tuples: `[string, number]`
|
|
1182
|
+
* - Unions: `string | number`
|
|
1183
|
+
* - Intersections: `A & B`
|
|
1184
|
+
* - Objects: `{ foo: string }`
|
|
1185
|
+
* - References: named interfaces
|
|
1186
|
+
*
|
|
1187
|
+
* @param type - The ts-morph Type to convert
|
|
1188
|
+
* @param visitingTypes - Set of type names currently being visited (for cycle detection)
|
|
1189
|
+
* @returns NormalizedType representation
|
|
1190
|
+
*/
|
|
1191
|
+
convertTypeToNormalized(type, visitingTypes = new Set()) {
|
|
1192
|
+
const typeToUse = type;
|
|
1193
|
+
// Check for primitives
|
|
1194
|
+
if (typeToUse.isString()) {
|
|
1195
|
+
return { kind: 'primitive', value: 'string' };
|
|
1196
|
+
}
|
|
1197
|
+
if (typeToUse.isNumber()) {
|
|
1198
|
+
return { kind: 'primitive', value: 'number' };
|
|
1199
|
+
}
|
|
1200
|
+
if (typeToUse.isBoolean()) {
|
|
1201
|
+
return { kind: 'primitive', value: 'boolean' };
|
|
1202
|
+
}
|
|
1203
|
+
if (typeToUse.isNull()) {
|
|
1204
|
+
return { kind: 'primitive', value: 'null' };
|
|
1205
|
+
}
|
|
1206
|
+
// Check for literal types
|
|
1207
|
+
if (typeToUse.isStringLiteral()) {
|
|
1208
|
+
return { kind: 'literal', value: typeToUse.getLiteralValue() };
|
|
1209
|
+
}
|
|
1210
|
+
if (typeToUse.isNumberLiteral()) {
|
|
1211
|
+
return { kind: 'literal', value: typeToUse.getLiteralValue() };
|
|
1212
|
+
}
|
|
1213
|
+
if (typeToUse.isBooleanLiteral()) {
|
|
1214
|
+
const text = typeToUse.getText();
|
|
1215
|
+
return { kind: 'literal', value: text === 'true' };
|
|
1216
|
+
}
|
|
1217
|
+
// Check for array types
|
|
1218
|
+
if (typeToUse.isArray()) {
|
|
1219
|
+
const elementType = typeToUse.getArrayElementType();
|
|
1220
|
+
if (elementType) {
|
|
1221
|
+
return {
|
|
1222
|
+
kind: 'array',
|
|
1223
|
+
element: this.convertTypeToNormalized(elementType, visitingTypes),
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
return { kind: 'array', element: { kind: 'unknown' } };
|
|
1227
|
+
}
|
|
1228
|
+
// Check for tuple types (convert to array for simplicity)
|
|
1229
|
+
if (typeToUse.isTuple()) {
|
|
1230
|
+
const tupleTypes = typeToUse.getTupleElements();
|
|
1231
|
+
if (tupleTypes.length > 0) {
|
|
1232
|
+
// For tuples, we lose type information - just use array
|
|
1233
|
+
return { kind: 'array', element: { kind: 'unknown' } };
|
|
1234
|
+
}
|
|
1235
|
+
return { kind: 'array', element: { kind: 'unknown' } };
|
|
1236
|
+
}
|
|
1237
|
+
// Check for union types
|
|
1238
|
+
if (typeToUse.isUnion()) {
|
|
1239
|
+
const variants = typeToUse.getUnionTypes()
|
|
1240
|
+
.filter(t => !t.isNull() && !t.isUndefined())
|
|
1241
|
+
.map(t => this.convertTypeToNormalized(t, visitingTypes));
|
|
1242
|
+
if (variants.length === 0) {
|
|
1243
|
+
return { kind: 'unknown' };
|
|
1244
|
+
}
|
|
1245
|
+
if (variants.length === 1) {
|
|
1246
|
+
return variants[0];
|
|
1247
|
+
}
|
|
1248
|
+
return { kind: 'union', variants };
|
|
1249
|
+
}
|
|
1250
|
+
// Check for intersection types
|
|
1251
|
+
if (typeToUse.isIntersection()) {
|
|
1252
|
+
const members = typeToUse.getIntersectionTypes()
|
|
1253
|
+
.map(t => this.convertTypeToNormalized(t, visitingTypes));
|
|
1254
|
+
return { kind: 'intersection', members };
|
|
1255
|
+
}
|
|
1256
|
+
// Check for object types (inline or interface references)
|
|
1257
|
+
if (typeToUse.isObject()) {
|
|
1258
|
+
// Check if it's a named interface reference
|
|
1259
|
+
const symbol = typeToUse.getSymbol();
|
|
1260
|
+
if (symbol) {
|
|
1261
|
+
const name = symbol.getName();
|
|
1262
|
+
// Skip built-in types but check for circular references
|
|
1263
|
+
if (name !== '__type' && name !== 'Array' && name !== 'Date') {
|
|
1264
|
+
// Check for circular reference - already visiting this type
|
|
1265
|
+
if (visitingTypes.has(name)) {
|
|
1266
|
+
return { kind: 'ref', name };
|
|
1267
|
+
}
|
|
1268
|
+
// Add to visiting set
|
|
1269
|
+
visitingTypes.add(name);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// Extract properties for inline object type
|
|
1273
|
+
const properties = {};
|
|
1274
|
+
const required = [];
|
|
1275
|
+
for (const prop of typeToUse.getProperties()) {
|
|
1276
|
+
const propName = prop.getName();
|
|
1277
|
+
const propDecl = prop.getDeclarations()[0];
|
|
1278
|
+
const propType = prop.getValueDeclaration()
|
|
1279
|
+
? prop.getValueDeclarationOrThrow().getType()
|
|
1280
|
+
: prop.getDeclaredType();
|
|
1281
|
+
const isOptional = prop.isOptional();
|
|
1282
|
+
const isReadonly = propDecl ? this.isPropertyReadonly(propDecl) : false;
|
|
1283
|
+
const jsDocInfo = propDecl ? this.getJSDocInfo(propDecl) : { description: undefined, deprecated: false };
|
|
1284
|
+
// Check for circular reference in property type
|
|
1285
|
+
const propSymbol = propType.getSymbol();
|
|
1286
|
+
if (propSymbol && visitingTypes.has(propSymbol.getName())) {
|
|
1287
|
+
properties[propName] = {
|
|
1288
|
+
type: { kind: 'ref', name: propSymbol.getName() },
|
|
1289
|
+
optional: isOptional,
|
|
1290
|
+
nullable: false,
|
|
1291
|
+
readonly: isReadonly,
|
|
1292
|
+
deprecated: jsDocInfo.deprecated,
|
|
1293
|
+
description: jsDocInfo.description,
|
|
1294
|
+
};
|
|
1295
|
+
if (!isOptional) {
|
|
1296
|
+
required.push(propName);
|
|
1297
|
+
}
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const { baseType, isNullable, hasUndefined } = this.analyzeNullability(propType);
|
|
1301
|
+
const normalizedPropType = this.convertTypeToNormalized(baseType, visitingTypes);
|
|
1302
|
+
properties[propName] = {
|
|
1303
|
+
type: normalizedPropType,
|
|
1304
|
+
optional: isOptional || hasUndefined,
|
|
1305
|
+
nullable: isNullable,
|
|
1306
|
+
readonly: isReadonly,
|
|
1307
|
+
deprecated: jsDocInfo.deprecated,
|
|
1308
|
+
description: jsDocInfo.description,
|
|
1309
|
+
};
|
|
1310
|
+
if (!isOptional && !hasUndefined) {
|
|
1311
|
+
required.push(propName);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
kind: 'object',
|
|
1316
|
+
schema: {
|
|
1317
|
+
properties,
|
|
1318
|
+
required,
|
|
1319
|
+
source: { source: 'typescript', id: 'inline' },
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
// Check for any/unknown
|
|
1324
|
+
if (typeToUse.getText() === 'any') {
|
|
1325
|
+
return { kind: 'any' };
|
|
1326
|
+
}
|
|
1327
|
+
return { kind: 'unknown' };
|
|
1328
|
+
}
|
|
1329
|
+
// --------------------------------------------------------------------------
|
|
1330
|
+
// Private: Property Metadata Extraction
|
|
1331
|
+
// --------------------------------------------------------------------------
|
|
1332
|
+
/**
|
|
1333
|
+
* Check if a property declaration is readonly.
|
|
1334
|
+
*
|
|
1335
|
+
* @param decl - The property declaration node
|
|
1336
|
+
* @returns True if the property has the readonly modifier
|
|
1337
|
+
*/
|
|
1338
|
+
isPropertyReadonly(decl) {
|
|
1339
|
+
if (Node.isPropertySignature(decl)) {
|
|
1340
|
+
return decl.isReadonly();
|
|
1341
|
+
}
|
|
1342
|
+
if (Node.isPropertyDeclaration(decl)) {
|
|
1343
|
+
return decl.isReadonly();
|
|
1344
|
+
}
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Extract JSDoc information from a declaration.
|
|
1349
|
+
*
|
|
1350
|
+
* Parses JSDoc comments to extract:
|
|
1351
|
+
* - Description text
|
|
1352
|
+
* - @deprecated tag presence
|
|
1353
|
+
*
|
|
1354
|
+
* @param decl - The declaration node to extract JSDoc from
|
|
1355
|
+
* @returns Object with description and deprecated flag
|
|
1356
|
+
*/
|
|
1357
|
+
getJSDocInfo(decl) {
|
|
1358
|
+
let description;
|
|
1359
|
+
let deprecated = false;
|
|
1360
|
+
// Get JSDoc from property signature or declaration
|
|
1361
|
+
if (Node.isPropertySignature(decl) || Node.isPropertyDeclaration(decl)) {
|
|
1362
|
+
const jsDocs = decl.getJsDocs();
|
|
1363
|
+
if (jsDocs.length > 0) {
|
|
1364
|
+
const jsDoc = jsDocs[0];
|
|
1365
|
+
const descText = jsDoc.getDescription().trim();
|
|
1366
|
+
if (descText) {
|
|
1367
|
+
description = descText;
|
|
1368
|
+
}
|
|
1369
|
+
// Check for @deprecated tag
|
|
1370
|
+
for (const tag of jsDoc.getTags()) {
|
|
1371
|
+
if (tag.getTagName() === 'deprecated') {
|
|
1372
|
+
deprecated = true;
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return { description, deprecated };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
//# sourceMappingURL=typescript.js.map
|