docorbit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +660 -0
  3. package/apps/cli/bin/docorbit.js +8 -0
  4. package/apps/cli/src/commands/add.ts +44 -0
  5. package/apps/cli/src/commands/api.ts +38 -0
  6. package/apps/cli/src/commands/context.ts +47 -0
  7. package/apps/cli/src/commands/dashboard.ts +55 -0
  8. package/apps/cli/src/commands/diff.ts +30 -0
  9. package/apps/cli/src/commands/evaluate.ts +133 -0
  10. package/apps/cli/src/commands/examples.ts +39 -0
  11. package/apps/cli/src/commands/export.ts +89 -0
  12. package/apps/cli/src/commands/impact.ts +31 -0
  13. package/apps/cli/src/commands/init.ts +69 -0
  14. package/apps/cli/src/commands/inspect.ts +30 -0
  15. package/apps/cli/src/commands/mcp.ts +72 -0
  16. package/apps/cli/src/commands/pitfalls.ts +38 -0
  17. package/apps/cli/src/commands/recipes.ts +35 -0
  18. package/apps/cli/src/commands/search.ts +48 -0
  19. package/apps/cli/src/commands/update.ts +73 -0
  20. package/apps/cli/src/commands/verify.ts +48 -0
  21. package/apps/cli/src/formatters/colors.ts +23 -0
  22. package/apps/cli/src/formatters/inspection.ts +102 -0
  23. package/apps/cli/src/formatters/knowledge.ts +272 -0
  24. package/apps/cli/src/formatters/retrieval.ts +74 -0
  25. package/apps/cli/src/formatters/terminal.ts +6 -0
  26. package/apps/cli/src/formatters/verification.ts +126 -0
  27. package/apps/cli/src/index.ts +409 -0
  28. package/bin/docorbit.js +8 -0
  29. package/package.json +46 -0
  30. package/packages/core/src/dashboard/server.ts +314 -0
  31. package/packages/core/src/dashboard/ui.ts +586 -0
  32. package/packages/core/src/implementation-service.ts +451 -0
  33. package/packages/core/src/index.ts +7 -0
  34. package/packages/core/src/inspector.ts +71 -0
  35. package/packages/core/src/pipeline.ts +331 -0
  36. package/packages/crawler/src/config.ts +12 -0
  37. package/packages/crawler/src/fetcher.ts +185 -0
  38. package/packages/crawler/src/index.ts +2 -0
  39. package/packages/discovery/src/index.ts +31 -0
  40. package/packages/discovery/src/provider.ts +47 -0
  41. package/packages/discovery/src/providers/generic.ts +98 -0
  42. package/packages/discovery/src/providers/github.ts +61 -0
  43. package/packages/discovery/src/providers/llms-txt.ts +73 -0
  44. package/packages/discovery/src/providers/markdown.ts +48 -0
  45. package/packages/discovery/src/providers/openapi.ts +91 -0
  46. package/packages/discovery/src/providers/sitemap.ts +62 -0
  47. package/packages/discovery/src/providers/skill.ts +54 -0
  48. package/packages/discovery/src/ranker.ts +123 -0
  49. package/packages/evaluation/src/dataset.ts +963 -0
  50. package/packages/evaluation/src/index.ts +8 -0
  51. package/packages/evaluation/src/runner.ts +241 -0
  52. package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
  53. package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
  54. package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
  55. package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
  56. package/packages/evaluation/src/types.ts +34 -0
  57. package/packages/evaluation/src/version-matcher.ts +73 -0
  58. package/packages/export/src/agents-md.ts +200 -0
  59. package/packages/export/src/claude-md.ts +141 -0
  60. package/packages/export/src/docs-map.ts +150 -0
  61. package/packages/export/src/index.ts +6 -0
  62. package/packages/export/src/llms-txt.ts +96 -0
  63. package/packages/export/src/service.ts +250 -0
  64. package/packages/export/src/skill-md.ts +128 -0
  65. package/packages/mcp/src/index.ts +46 -0
  66. package/packages/mcp/src/resources/index.ts +189 -0
  67. package/packages/mcp/src/server.ts +278 -0
  68. package/packages/mcp/src/tools/analyze-impact.ts +74 -0
  69. package/packages/mcp/src/tools/check-api.ts +86 -0
  70. package/packages/mcp/src/tools/diff-docs.ts +68 -0
  71. package/packages/mcp/src/tools/export-context.ts +73 -0
  72. package/packages/mcp/src/tools/find-api.ts +99 -0
  73. package/packages/mcp/src/tools/find-example.ts +100 -0
  74. package/packages/mcp/src/tools/find-pitfall.ts +94 -0
  75. package/packages/mcp/src/tools/find-recipe.ts +98 -0
  76. package/packages/mcp/src/tools/get-doc.ts +130 -0
  77. package/packages/mcp/src/tools/get-docs-map.ts +64 -0
  78. package/packages/mcp/src/tools/get-version.ts +118 -0
  79. package/packages/mcp/src/tools/implementation-context.ts +88 -0
  80. package/packages/mcp/src/tools/index.ts +59 -0
  81. package/packages/mcp/src/tools/list-sources.ts +85 -0
  82. package/packages/mcp/src/tools/search-docs.ts +123 -0
  83. package/packages/mcp/src/tools/types.ts +28 -0
  84. package/packages/mcp/src/transports/http.ts +256 -0
  85. package/packages/mcp/src/transports/stdio.ts +105 -0
  86. package/packages/mcp/src/transports/types.ts +6 -0
  87. package/packages/mcp/src/types.ts +102 -0
  88. package/packages/normalizer/src/example-indexer.ts +240 -0
  89. package/packages/normalizer/src/html.ts +253 -0
  90. package/packages/normalizer/src/index.ts +8 -0
  91. package/packages/normalizer/src/llms.ts +83 -0
  92. package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
  93. package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
  94. package/packages/normalizer/src/openapi.ts +2 -0
  95. package/packages/normalizer/src/page.ts +184 -0
  96. package/packages/normalizer/src/pitfall-extractor.ts +190 -0
  97. package/packages/normalizer/src/slicer.ts +455 -0
  98. package/packages/retrieval/src/engine.ts +120 -0
  99. package/packages/retrieval/src/index.ts +7 -0
  100. package/packages/retrieval/src/intent.ts +43 -0
  101. package/packages/retrieval/src/packer.ts +145 -0
  102. package/packages/retrieval/src/recipe-engine.ts +313 -0
  103. package/packages/retrieval/src/scorer.ts +139 -0
  104. package/packages/retrieval/src/weights.ts +31 -0
  105. package/packages/security/src/annotations.ts +112 -0
  106. package/packages/security/src/index.ts +2 -0
  107. package/packages/security/src/ssrf.ts +153 -0
  108. package/packages/shared/src/errors.ts +53 -0
  109. package/packages/shared/src/hashing.ts +23 -0
  110. package/packages/shared/src/index.ts +3 -0
  111. package/packages/shared/src/types.ts +881 -0
  112. package/packages/storage/src/db.ts +72 -0
  113. package/packages/storage/src/index.ts +11 -0
  114. package/packages/storage/src/interfaces.ts +115 -0
  115. package/packages/storage/src/repositories/api-repository.ts +219 -0
  116. package/packages/storage/src/repositories/chunk-repository.ts +316 -0
  117. package/packages/storage/src/repositories/example-repository.ts +206 -0
  118. package/packages/storage/src/repositories/page-repository.ts +205 -0
  119. package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
  120. package/packages/storage/src/repositories/source-repository.ts +205 -0
  121. package/packages/storage/src/repository.ts +256 -0
  122. package/packages/storage/src/schema.ts +269 -0
  123. package/packages/storage/src/search-tokens.ts +28 -0
  124. package/packages/verification/src/diff-engine.ts +258 -0
  125. package/packages/verification/src/extractor.ts +339 -0
  126. package/packages/verification/src/impact-scanner.ts +203 -0
  127. package/packages/verification/src/index.ts +5 -0
  128. package/packages/verification/src/services.ts +238 -0
  129. package/packages/verification/src/verifier.ts +375 -0
  130. package/packages/workspace/src/detector.ts +143 -0
  131. package/packages/workspace/src/ecosystems/cargo.ts +84 -0
  132. package/packages/workspace/src/ecosystems/composer.ts +42 -0
  133. package/packages/workspace/src/ecosystems/go.ts +54 -0
  134. package/packages/workspace/src/ecosystems/index.ts +34 -0
  135. package/packages/workspace/src/ecosystems/maven.ts +34 -0
  136. package/packages/workspace/src/ecosystems/npm.ts +83 -0
  137. package/packages/workspace/src/ecosystems/pub.ts +40 -0
  138. package/packages/workspace/src/ecosystems/pypi.ts +100 -0
  139. package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
  140. package/packages/workspace/src/ecosystems/types.ts +18 -0
  141. package/packages/workspace/src/index.ts +5 -0
  142. package/packages/workspace/src/lockfile.ts +194 -0
  143. package/packages/workspace/src/resolver.ts +234 -0
  144. package/packages/workspace/src/semver.ts +259 -0
@@ -0,0 +1,59 @@
1
+ import type { McpToolHandler } from './types.ts';
2
+ import { SearchDocsTool } from './search-docs.ts';
3
+ import { GetDocTool } from './get-doc.ts';
4
+ import { FindApiTool } from './find-api.ts';
5
+ import { FindExampleTool } from './find-example.ts';
6
+ import { FindPitfallTool } from './find-pitfall.ts';
7
+ import { FindRecipeTool } from './find-recipe.ts';
8
+ import { GetVersionTool } from './get-version.ts';
9
+ import { ListSourcesTool } from './list-sources.ts';
10
+ import { ImplementationContextTool } from './implementation-context.ts';
11
+ import { CheckApiTool } from './check-api.ts';
12
+ import { DiffDocsTool } from './diff-docs.ts';
13
+ import { AnalyzeImpactTool } from './analyze-impact.ts';
14
+ import { GetDocumentationMapTool } from './get-docs-map.ts';
15
+ import { ExportAgentContextTool } from './export-context.ts';
16
+
17
+ export * from './types.ts';
18
+ export {
19
+ SearchDocsTool,
20
+ GetDocTool,
21
+ FindApiTool,
22
+ FindExampleTool,
23
+ FindPitfallTool,
24
+ FindRecipeTool,
25
+ GetVersionTool,
26
+ ListSourcesTool,
27
+ ImplementationContextTool,
28
+ ImplementationContextTool as GetImplementationContextTool,
29
+ CheckApiTool,
30
+ DiffDocsTool,
31
+ AnalyzeImpactTool,
32
+ GetDocumentationMapTool,
33
+ ExportAgentContextTool,
34
+ };
35
+
36
+ export function createDefaultTools(): Map<string, McpToolHandler> {
37
+ const tools = new Map<string, McpToolHandler>();
38
+ const list: McpToolHandler[] = [
39
+ new SearchDocsTool(),
40
+ new GetDocTool(),
41
+ new FindApiTool(),
42
+ new FindExampleTool(),
43
+ new FindPitfallTool(),
44
+ new FindRecipeTool(),
45
+ new GetVersionTool(),
46
+ new ListSourcesTool(),
47
+ new ImplementationContextTool(),
48
+ new CheckApiTool(),
49
+ new DiffDocsTool(),
50
+ new AnalyzeImpactTool(),
51
+ new GetDocumentationMapTool(),
52
+ new ExportAgentContextTool(),
53
+ ];
54
+ for (const tool of list) {
55
+ tools.set(tool.definition.name, tool);
56
+ }
57
+ return tools;
58
+ }
59
+
@@ -0,0 +1,85 @@
1
+ import type { CallToolResult, McpTool } from '../types.ts';
2
+ import type { McpContext, McpToolHandler } from './types.ts';
3
+
4
+ export class ListSourcesTool implements McpToolHandler {
5
+ readonly definition: McpTool = {
6
+ name: 'list_sources',
7
+ description: 'List all indexed documentation sources, snapshot records, doc versions, and machine-readability status.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ limit: {
12
+ type: 'number',
13
+ description: 'Maximum number of sources to list (default: 50).',
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ async execute(args: Record<string, unknown>, ctx: McpContext): Promise<CallToolResult> {
20
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 50;
21
+
22
+ const sources = ctx.repo.listSources().slice(0, limit);
23
+ const snapshots = ctx.repo.listSnapshots();
24
+
25
+ if (sources.length === 0) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: JSON.stringify({
31
+ markdown: '### Indexed Documentation Sources\n\nNo documentation sources have been added yet. Use `docorbit add <url>` to ingest documentation.',
32
+ data: { count: 0, sources: [] },
33
+ }, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+
39
+ const formatted = sources.map(s => {
40
+ const sourceSnaps = snapshots.filter(sn => sn.sourceId === s.id);
41
+ return {
42
+ id: s.id,
43
+ url: s.url,
44
+ type: s.type,
45
+ status: s.status,
46
+ authority: s.authority,
47
+ confidence: s.confidence,
48
+ machineReadable: s.machineReadable,
49
+ snapshots: sourceSnaps.map(sn => ({
50
+ id: sn.id,
51
+ pageCount: sn.pageCount,
52
+ docVersion: sn.docVersion,
53
+ capturedAt: sn.capturedAt,
54
+ })),
55
+ };
56
+ });
57
+
58
+ const lines: string[] = [`### Indexed Documentation Sources (${sources.length} sources)\n`];
59
+ for (const s of formatted) {
60
+ const icon = s.status === 'valid' ? '✓' : '✗';
61
+ lines.push(`- **${icon} [${s.type}]** ${s.url}`);
62
+ lines.push(` *Authority*: ${s.authority} | *Confidence*: ${Math.round(s.confidence * 100)}% | *Machine-Readable*: ${s.machineReadable}`);
63
+ if (s.snapshots.length > 0) {
64
+ const snapInfo = s.snapshots.map(sn => `${sn.id}${sn.docVersion ? ` (${sn.docVersion})` : ''} [${sn.pageCount} pages]`).join(', ');
65
+ lines.push(` *Snapshots*: ${snapInfo}`);
66
+ }
67
+ lines.push('');
68
+ }
69
+
70
+ return {
71
+ content: [
72
+ {
73
+ type: 'text',
74
+ text: JSON.stringify({
75
+ markdown: lines.join('\n'),
76
+ data: {
77
+ count: formatted.length,
78
+ sources: formatted,
79
+ },
80
+ }, null, 2),
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ }
@@ -0,0 +1,123 @@
1
+ import { RetrievalEngine } from '../../../retrieval/src/index.ts';
2
+ import type { CallToolResult, McpTool } from '../types.ts';
3
+ import type { McpContext, McpToolHandler } from './types.ts';
4
+
5
+ export class SearchDocsTool implements McpToolHandler {
6
+ readonly definition: McpTool = {
7
+ name: 'search_docs',
8
+ description: 'Search documentation chunks using hybrid FTS5 ranking, symbol awareness, and optional version filtering.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ query: {
13
+ type: 'string',
14
+ description: 'The search query, code symbol, or concept to look for.',
15
+ },
16
+ library: {
17
+ type: 'string',
18
+ description: 'Optional library name to narrow search scope.',
19
+ },
20
+ version: {
21
+ type: 'string',
22
+ description: 'Target documentation version (e.g. "v14", "15.0").',
23
+ },
24
+ limit: {
25
+ type: 'number',
26
+ description: 'Maximum number of results to return (default: 10).',
27
+ },
28
+ },
29
+ required: ['query'],
30
+ },
31
+ };
32
+
33
+ async execute(args: Record<string, unknown>, ctx: McpContext): Promise<CallToolResult> {
34
+ const query = typeof args.query === 'string' ? args.query.trim() : '';
35
+ if (!query) {
36
+ return {
37
+ isError: true,
38
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Missing required parameter: query' }) }],
39
+ };
40
+ }
41
+
42
+ const version = typeof args.docVersion === 'string'
43
+ ? args.docVersion
44
+ : (typeof args.version === 'string' ? args.version : undefined);
45
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 10;
46
+
47
+ const engine = new RetrievalEngine(ctx.repo);
48
+ const results = await engine.search(query, {
49
+ docVersion: version,
50
+ limit,
51
+ projectDir: ctx.workspaceRoot,
52
+ });
53
+
54
+ if (results.length === 0) {
55
+ const emptyPayload = {
56
+ query,
57
+ count: 0,
58
+ results: [],
59
+ message: `No documentation chunks found matching "${query}". Try refining terms or checking available sources.`,
60
+ };
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: JSON.stringify({
66
+ markdown: `### Search Results for "${query}"\n\nNo matching documentation found.`,
67
+ data: emptyPayload,
68
+ }, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ const formattedChunks = results.map(r => ({
75
+ id: r.chunk.id,
76
+ chunk: r.chunk,
77
+ title: r.chunk.title || 'Untitled Section',
78
+ sectionPath: r.chunk.sectionPath,
79
+ content: r.chunk.content,
80
+ chunkType: r.chunk.chunkType,
81
+ language: r.chunk.language,
82
+ tokenEstimate: r.chunk.tokenEstimate,
83
+ docVersion: r.chunk.docVersion,
84
+ score: r.score,
85
+ symbols: r.symbols.map(s => s.name),
86
+ provenance: r.chunk.provenance ? {
87
+ sourceUrl: r.chunk.provenance.sourceUrl,
88
+ fetchedAt: r.chunk.provenance.fetchedAt,
89
+ untrusted: true,
90
+ } : undefined,
91
+ }));
92
+
93
+ const lines: string[] = [
94
+ `### Search Results for "${query}" (${results.length} found)`,
95
+ `> [!NOTE] External documentation content is untrusted.\n`,
96
+ ];
97
+
98
+ for (let i = 0; i < formattedChunks.length; i++) {
99
+ const c = formattedChunks[i];
100
+ const path = c.sectionPath.length > 0 ? c.sectionPath.join(' > ') : c.title;
101
+ lines.push(`${i + 1}. **${path}** [${c.chunkType}] (score: ${c.score})`);
102
+ lines.push(` *ID*: \`${c.id}\`${c.docVersion ? ` | *Version*: \`${c.docVersion}\`` : ''}`);
103
+ if (c.symbols.length > 0) lines.push(` *Symbols*: ${c.symbols.join(', ')}`);
104
+ lines.push(` \`\`\`\n ${c.content.split('\n').slice(0, 5).join('\n ')}\n \`\`\`\n`);
105
+ }
106
+
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: JSON.stringify({
112
+ markdown: lines.join('\n'),
113
+ data: {
114
+ query,
115
+ count: results.length,
116
+ results: formattedChunks,
117
+ },
118
+ }, null, 2),
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ }
@@ -0,0 +1,28 @@
1
+ import type { DocOrbitRepository } from '../../../storage/src/index.ts';
2
+ import type { ImplementationContextService } from '../../../core/src/index.ts';
3
+ import type { WorkspaceResolver } from '../../../workspace/src/index.ts';
4
+ import type {
5
+ VerificationService,
6
+ DiffService,
7
+ ImpactAnalysisService,
8
+ } from '../../../verification/src/index.ts';
9
+ import type { ExportService } from '../../../export/src/index.ts';
10
+ import type { CallToolResult, McpTool } from '../types.ts';
11
+
12
+ export interface McpContext {
13
+ repo: DocOrbitRepository;
14
+ implService: ImplementationContextService;
15
+ implementationService: ImplementationContextService;
16
+ workspaceRoot?: string;
17
+ projectDir?: string;
18
+ resolver?: WorkspaceResolver;
19
+ verificationService?: VerificationService;
20
+ diffService?: DiffService;
21
+ impactService?: ImpactAnalysisService;
22
+ exportService?: ExportService;
23
+ }
24
+
25
+ export interface McpToolHandler {
26
+ readonly definition: McpTool;
27
+ execute(args: Record<string, unknown>, ctx: McpContext): Promise<CallToolResult>;
28
+ }
@@ -0,0 +1,256 @@
1
+ import * as http from 'node:http';
2
+ import type { McpServer } from '../server.ts';
3
+ import type { McpTransport } from './types.ts';
4
+ import { JSONRPC_ERRORS } from '../types.ts';
5
+
6
+ export interface HttpTransportOptions {
7
+ port?: number;
8
+ host?: string;
9
+ maxBodyBytes?: number;
10
+ noListen?: boolean;
11
+ }
12
+
13
+ export class StreamableHttpTransport implements McpTransport {
14
+ private port: number;
15
+ private host: string;
16
+ private maxBodyBytes: number;
17
+ private noListen: boolean;
18
+ private serverInstance: http.Server | null = null;
19
+ private mcpServer: McpServer | null = null;
20
+ private sseClients: Set<http.ServerResponse> = new Set();
21
+ private heartbeatTimer: NodeJS.Timeout | null = null;
22
+
23
+ constructor(options: HttpTransportOptions = {}) {
24
+ this.port = options.port ?? 3000;
25
+ this.host = options.host ?? '127.0.0.1';
26
+ this.maxBodyBytes = options.maxBodyBytes ?? 10 * 1024 * 1024; // 10MB
27
+ this.noListen = options.noListen ?? false;
28
+ }
29
+
30
+ getServerInstance(): http.Server | null {
31
+ return this.serverInstance;
32
+ }
33
+
34
+ async dispatch(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
35
+ return this.handleHttpRequest(req, res);
36
+ }
37
+
38
+ getPort(): number {
39
+ if (this.serverInstance) {
40
+ const addr = this.serverInstance.address();
41
+ if (addr && typeof addr === 'object') {
42
+ return addr.port;
43
+ }
44
+ }
45
+ return this.port;
46
+ }
47
+
48
+ async start(server: McpServer): Promise<void> {
49
+ this.mcpServer = server;
50
+
51
+ this.serverInstance = http.createServer((req, res) => {
52
+ this.handleHttpRequest(req, res).catch(err => {
53
+ if (!res.headersSent) {
54
+ res.writeHead(500, { 'Content-Type': 'application/json' });
55
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
56
+ }
57
+ });
58
+ });
59
+
60
+ if (this.noListen) {
61
+ return;
62
+ }
63
+
64
+ return new Promise((resolve, reject) => {
65
+ this.serverInstance!.on('error', reject);
66
+
67
+ this.serverInstance!.listen(this.port, this.host, () => {
68
+ this.startHeartbeats();
69
+ resolve();
70
+ });
71
+ });
72
+ }
73
+
74
+ private setCorsHeaders(res: http.ServerResponse): void {
75
+ res.setHeader('Access-Control-Allow-Origin', '*');
76
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
77
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
78
+ }
79
+
80
+ private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
81
+ this.setCorsHeaders(res);
82
+
83
+ if (req.method === 'OPTIONS') {
84
+ res.writeHead(204);
85
+ res.end();
86
+ return;
87
+ }
88
+
89
+ const url = new URL(req.url || '/', `http://${this.host}:${this.port}`);
90
+ const pathname = url.pathname;
91
+
92
+ if (req.method === 'GET' && (pathname === '/health' || pathname === '/')) {
93
+ res.writeHead(200, { 'Content-Type': 'application/json' });
94
+ res.end(JSON.stringify({
95
+ status: 'ok',
96
+ server: 'docorbit-mcp',
97
+ version: '0.5.0',
98
+ endpoints: {
99
+ mcp: '/mcp',
100
+ sse: '/sse',
101
+ health: '/health',
102
+ },
103
+ }));
104
+ return;
105
+ }
106
+
107
+ if (req.method === 'GET' && pathname === '/sse') {
108
+ this.handleSseConnection(req, res);
109
+ return;
110
+ }
111
+
112
+ if (req.method === 'POST' && pathname === '/mcp') {
113
+ await this.handleMcpPost(req, res);
114
+ return;
115
+ }
116
+
117
+ res.writeHead(404, { 'Content-Type': 'application/json' });
118
+ res.end(JSON.stringify({ error: `Not found: ${req.method} ${pathname}` }));
119
+ }
120
+
121
+ private handleSseConnection(req: http.IncomingMessage, res: http.ServerResponse): void {
122
+ res.writeHead(200, {
123
+ 'Content-Type': 'text/event-stream',
124
+ 'Cache-Control': 'no-cache',
125
+ 'Connection': 'keep-alive',
126
+ });
127
+
128
+ // Send endpoint notification so client knows where to POST messages
129
+ res.write('event: endpoint\ndata: /mcp\n\n');
130
+
131
+ this.sseClients.add(res);
132
+
133
+ req.on('close', () => {
134
+ this.sseClients.delete(res);
135
+ });
136
+ }
137
+
138
+ private async handleMcpPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
139
+ const rawBody = await this.readBody(req);
140
+ let parsed: unknown;
141
+ try {
142
+ parsed = JSON.parse(rawBody);
143
+ } catch {
144
+ res.writeHead(400, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({
146
+ jsonrpc: '2.0',
147
+ id: null,
148
+ error: {
149
+ code: JSONRPC_ERRORS.PARSE_ERROR,
150
+ message: 'Parse error: invalid JSON payload.',
151
+ },
152
+ }));
153
+ return;
154
+ }
155
+
156
+ if (!this.mcpServer) {
157
+ res.writeHead(503, { 'Content-Type': 'application/json' });
158
+ res.end(JSON.stringify({ error: 'Server not initialized' }));
159
+ return;
160
+ }
161
+
162
+ let responsePayload: unknown;
163
+ if (Array.isArray(parsed)) {
164
+ const responses = await Promise.all(parsed.map(item => this.mcpServer!.handleMessage(item)));
165
+ responsePayload = responses.filter(r => r !== null);
166
+ } else {
167
+ responsePayload = await this.mcpServer.handleMessage(parsed);
168
+ }
169
+
170
+ const acceptHeader = req.headers['accept'] || '';
171
+ const wantsSse = acceptHeader.includes('text/event-stream');
172
+
173
+ if (wantsSse) {
174
+ // Modern Streamable HTTP response over SSE chunk
175
+ res.writeHead(200, {
176
+ 'Content-Type': 'text/event-stream',
177
+ 'Cache-Control': 'no-cache',
178
+ 'Connection': 'keep-alive',
179
+ });
180
+ if (responsePayload !== null && responsePayload !== undefined) {
181
+ res.write(`event: message\ndata: ${JSON.stringify(responsePayload)}\n\n`);
182
+ }
183
+ res.end();
184
+ return;
185
+ }
186
+
187
+ // Standard JSON response
188
+ if (responsePayload === null || responsePayload === undefined) {
189
+ res.writeHead(204);
190
+ res.end();
191
+ return;
192
+ }
193
+
194
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
195
+ res.end(JSON.stringify(responsePayload));
196
+ }
197
+
198
+ private readBody(req: http.IncomingMessage): Promise<string> {
199
+ return new Promise((resolve, reject) => {
200
+ const chunks: Buffer[] = [];
201
+ let totalLength = 0;
202
+
203
+ req.on('data', chunk => {
204
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
205
+ totalLength += buf.length;
206
+ if (totalLength > this.maxBodyBytes) {
207
+ req.destroy(new Error(`Payload exceeds maximum limit of ${this.maxBodyBytes} bytes`));
208
+ return;
209
+ }
210
+ chunks.push(buf);
211
+ });
212
+
213
+ req.on('end', () => {
214
+ resolve(Buffer.concat(chunks).toString('utf-8'));
215
+ });
216
+
217
+ req.on('error', reject);
218
+ });
219
+ }
220
+
221
+ private startHeartbeats(): void {
222
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
223
+ this.heartbeatTimer = setInterval(() => {
224
+ for (const client of this.sseClients) {
225
+ try {
226
+ client.write(': ping\n\n');
227
+ } catch {
228
+ this.sseClients.delete(client);
229
+ }
230
+ }
231
+ }, 15000);
232
+ }
233
+
234
+ async close(): Promise<void> {
235
+ if (this.heartbeatTimer) {
236
+ clearInterval(this.heartbeatTimer);
237
+ this.heartbeatTimer = null;
238
+ }
239
+
240
+ for (const client of this.sseClients) {
241
+ try {
242
+ client.end();
243
+ } catch {
244
+ // Continue
245
+ }
246
+ }
247
+ this.sseClients.clear();
248
+
249
+ if (this.serverInstance) {
250
+ await new Promise<void>((resolve) => {
251
+ this.serverInstance!.close(() => resolve());
252
+ });
253
+ this.serverInstance = null;
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,105 @@
1
+ import * as readline from 'node:readline';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import type { McpServer } from '../server.ts';
4
+ import type { McpTransport } from './types.ts';
5
+ import { JSONRPC_ERRORS } from '../types.ts';
6
+
7
+ export interface StdioTransportOptions {
8
+ input?: Readable;
9
+ output?: Writable;
10
+ errorOutput?: Writable;
11
+ }
12
+
13
+ export class StdioServerTransport implements McpTransport {
14
+ private input: Readable;
15
+ private output: Writable;
16
+ private errorOutput: Writable;
17
+ private rl: readline.Interface | null = null;
18
+ private server: McpServer | null = null;
19
+ private running: boolean = false;
20
+
21
+ constructor(options: StdioTransportOptions = {}) {
22
+ this.input = options.input || process.stdin;
23
+ this.output = options.output || process.stdout;
24
+ this.errorOutput = options.errorOutput || process.stderr;
25
+ }
26
+
27
+ async start(server: McpServer): Promise<void> {
28
+ this.server = server;
29
+ this.running = true;
30
+
31
+ this.rl = readline.createInterface({
32
+ input: this.input,
33
+ terminal: false,
34
+ });
35
+
36
+ this.log('[DocOrbit MCP] Stdio transport started.');
37
+
38
+ this.rl.on('line', async (line: string) => {
39
+ try {
40
+ await this.processLine(line);
41
+ } catch (err) {
42
+ this.log(`[DocOrbit MCP] Stdio error processing line: ${err instanceof Error ? err.message : String(err)}`);
43
+ }
44
+ });
45
+
46
+ this.rl.on('close', () => {
47
+ this.running = false;
48
+ this.log('[DocOrbit MCP] Stdio transport stream closed.');
49
+ });
50
+ }
51
+
52
+ private async processLine(line: string): Promise<void> {
53
+ let parsed: unknown;
54
+ try {
55
+ parsed = JSON.parse(line);
56
+ } catch {
57
+ const parseError = {
58
+ jsonrpc: '2.0',
59
+ id: null,
60
+ error: {
61
+ code: JSONRPC_ERRORS.PARSE_ERROR,
62
+ message: 'Parse error: invalid JSON payload.',
63
+ },
64
+ };
65
+ this.send(parseError);
66
+ return;
67
+ }
68
+
69
+ if (!this.server) return;
70
+
71
+ if (Array.isArray(parsed)) {
72
+ // JSON-RPC Batch
73
+ const responses = await Promise.all(parsed.map(item => this.server!.handleMessage(item)));
74
+ const filtered = responses.filter(r => r !== null);
75
+ if (filtered.length > 0) {
76
+ this.send(filtered);
77
+ }
78
+ return;
79
+ }
80
+
81
+ const response = await this.server.handleMessage(parsed);
82
+ if (response !== null) {
83
+ this.send(response);
84
+ }
85
+ }
86
+
87
+ private send(payload: unknown): void {
88
+ if (!this.running) return;
89
+ const serialized = JSON.stringify(payload) + '\n';
90
+ this.output.write(serialized);
91
+ }
92
+
93
+ private log(message: string): void {
94
+ // Isolated to stderr to never corrupt stdout JSON-RPC stream
95
+ this.errorOutput.write(`${message}\n`);
96
+ }
97
+
98
+ async close(): Promise<void> {
99
+ this.running = false;
100
+ if (this.rl) {
101
+ this.rl.close();
102
+ this.rl = null;
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,6 @@
1
+ import type { McpServer } from '../server.ts';
2
+
3
+ export interface McpTransport {
4
+ start(server: McpServer): Promise<void>;
5
+ close(): Promise<void>;
6
+ }