@atlascompiler/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.
@@ -0,0 +1,1164 @@
1
+ // src/errors.ts
2
+ var AtlasMcpError = class extends Error {
3
+ code;
4
+ retryable;
5
+ retryAfterSeconds;
6
+ details;
7
+ constructor(payload) {
8
+ super(`[${payload.code}] ${payload.message}`);
9
+ this.name = "AtlasMcpError";
10
+ this.code = payload.code;
11
+ this.retryable = payload.retryable;
12
+ this.retryAfterSeconds = payload.retryAfterSeconds;
13
+ this.details = payload.details;
14
+ }
15
+ toToolErrorResult() {
16
+ return {
17
+ isError: true,
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: `Atlas Error (${this.code}): ${this.message}${this.retryAfterSeconds ? ` (Retry after ${this.retryAfterSeconds}s)` : ""}`
22
+ }
23
+ ],
24
+ structuredContent: {
25
+ error: {
26
+ code: this.code,
27
+ message: this.message,
28
+ retryable: this.retryable,
29
+ retry_after_seconds: this.retryAfterSeconds,
30
+ details: this.details
31
+ }
32
+ }
33
+ };
34
+ }
35
+ };
36
+ function wrapFetchError(err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ return new AtlasMcpError({
39
+ code: msg.includes("timeout") ? "ATLAS_FETCH_TIMEOUT" : "ATLAS_INTERNAL_ERROR",
40
+ message: `Failed to connect to Atlas API: ${msg}`,
41
+ retryable: true
42
+ });
43
+ }
44
+ async function assertOkResponse(response) {
45
+ if (response.ok) {
46
+ return;
47
+ }
48
+ let errorDetail = `HTTP ${response.status}`;
49
+ try {
50
+ const errJson = await response.json();
51
+ errorDetail = errJson.detail || errJson.title || errorDetail;
52
+ } catch {
53
+ }
54
+ if (response.status === 401) {
55
+ throw new AtlasMcpError({
56
+ code: "ATLAS_UNAUTHORIZED",
57
+ message: "Unauthorized: missing or invalid ATLAS_API_KEY.",
58
+ retryable: false
59
+ });
60
+ }
61
+ if (response.status === 402 || response.status === 403) {
62
+ throw new AtlasMcpError({
63
+ code: "ATLAS_QUOTA_EXCEEDED",
64
+ message: `Atlas quota or authorization error: ${errorDetail}`,
65
+ retryable: false
66
+ });
67
+ }
68
+ if (response.status === 429) {
69
+ throw new AtlasMcpError({
70
+ code: "ATLAS_RATE_LIMITED",
71
+ message: `Atlas rate limit exceeded: ${errorDetail}`,
72
+ retryable: true,
73
+ retryAfterSeconds: 5
74
+ });
75
+ }
76
+ if (response.status === 400 && errorDetail.toLowerCase().includes("ssrf")) {
77
+ throw new AtlasMcpError({
78
+ code: "ATLAS_SSRF_BLOCKED",
79
+ message: errorDetail,
80
+ retryable: false
81
+ });
82
+ }
83
+ if (response.status === 404) {
84
+ throw new AtlasMcpError({
85
+ code: "ATLAS_RESOURCE_NOT_FOUND",
86
+ message: `Resource not found: ${errorDetail}`,
87
+ retryable: false
88
+ });
89
+ }
90
+ throw new AtlasMcpError({
91
+ code: "ATLAS_FETCH_HTTP_ERROR",
92
+ message: `Atlas API error: ${errorDetail}`,
93
+ retryable: response.status >= 500
94
+ });
95
+ }
96
+ function formatErrorToToolResult(err) {
97
+ if (err instanceof AtlasMcpError) {
98
+ return err.toToolErrorResult();
99
+ }
100
+ let p = typeof err === "object" && err !== null ? err : null;
101
+ if (p && typeof p.message === "string" && p.message.startsWith("{") && p.message.endsWith("}")) {
102
+ try {
103
+ const parsed = JSON.parse(p.message);
104
+ if (typeof parsed === "object" && parsed !== null) {
105
+ p = { ...p, ...parsed };
106
+ }
107
+ } catch {
108
+ }
109
+ }
110
+ if (p) {
111
+ const status = typeof p.status === "number" ? p.status : void 0;
112
+ const code = typeof p.code === "string" ? p.code : void 0;
113
+ const title = typeof p.title === "string" ? p.title : void 0;
114
+ const detail = typeof p.detail === "string" ? p.detail : void 0;
115
+ const message2 = detail || title || (typeof p.message === "string" ? p.message : "Unknown Atlas error");
116
+ if (status === 401 || code === "unauthorized" || code === "invalid_credentials") {
117
+ return new AtlasMcpError({
118
+ code: "ATLAS_UNAUTHORIZED",
119
+ message: message2,
120
+ retryable: false
121
+ }).toToolErrorResult();
122
+ }
123
+ if (status === 429 || code === "execution_concurrency" || code === "rate_limit_exceeded") {
124
+ return new AtlasMcpError({
125
+ code: "ATLAS_RATE_LIMITED",
126
+ message: code === "execution_concurrency" ? "Execution concurrency limit reached for your workspace. Please retry shortly." : message2,
127
+ retryable: true,
128
+ retryAfterSeconds: 5
129
+ }).toToolErrorResult();
130
+ }
131
+ if (status === 402 || code === "quota_exceeded" || code === "insufficient_credits") {
132
+ return new AtlasMcpError({
133
+ code: "ATLAS_QUOTA_EXCEEDED",
134
+ message: message2,
135
+ retryable: false
136
+ }).toToolErrorResult();
137
+ }
138
+ if (status === 404 || code === "resource_not_found") {
139
+ return new AtlasMcpError({
140
+ code: "ATLAS_RESOURCE_NOT_FOUND",
141
+ message: message2,
142
+ retryable: false
143
+ }).toToolErrorResult();
144
+ }
145
+ if (message2.toLowerCase().includes("ssrf") || code === "ssrf_blocked" || message2.toLowerCase().includes("private ip")) {
146
+ return new AtlasMcpError({
147
+ code: "ATLAS_SSRF_BLOCKED",
148
+ message: message2,
149
+ retryable: false
150
+ }).toToolErrorResult();
151
+ }
152
+ }
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ return {
155
+ isError: true,
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: `Atlas Execution Error: ${message}`
160
+ }
161
+ ],
162
+ structuredContent: {
163
+ error: {
164
+ code: "ATLAS_INTERNAL_ERROR",
165
+ message,
166
+ retryable: false
167
+ }
168
+ }
169
+ };
170
+ }
171
+
172
+ // src/resources.ts
173
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
174
+ var compilationResourceTemplate = new ResourceTemplate("atlas://compilations/{id}", {
175
+ list: void 0
176
+ });
177
+ var sectionResourceTemplate = new ResourceTemplate(
178
+ "atlas://compilations/{id}/sections/{section_id}",
179
+ { list: void 0 }
180
+ );
181
+ var crawlResourceTemplate = new ResourceTemplate("atlas://crawls/{id}", {
182
+ list: void 0
183
+ });
184
+ var astResourceTemplate = new ResourceTemplate("atlas://compilations/{id}/ast", {
185
+ list: void 0
186
+ });
187
+ function createRemoteResourceReader(context) {
188
+ return async (uri, _variables) => {
189
+ const baseUrl = (context.baseUrl ?? "https://api.atlas-compiler.com").replace(/\/+$/, "");
190
+ const fetchFn = context.fetch ?? fetch;
191
+ const headers = {
192
+ "Content-Type": "application/json",
193
+ Accept: "application/json, text/event-stream"
194
+ };
195
+ if (context.apiKey) {
196
+ headers.Authorization = `Bearer ${context.apiKey}`;
197
+ }
198
+ let response;
199
+ try {
200
+ response = await fetchFn(`${baseUrl}/mcp`, {
201
+ method: "POST",
202
+ headers,
203
+ body: JSON.stringify({
204
+ jsonrpc: "2.0",
205
+ id: 1,
206
+ method: "resources/read",
207
+ params: { uri }
208
+ }),
209
+ signal: AbortSignal.timeout(15e3)
210
+ });
211
+ } catch (err) {
212
+ throw wrapFetchError(err);
213
+ }
214
+ if (!response.ok) {
215
+ if (response.status === 404 || response.status === 403) {
216
+ throw new AtlasMcpError({
217
+ code: "ATLAS_RESOURCE_NOT_FOUND",
218
+ message: `Resource not found or unauthorized: ${uri}`,
219
+ retryable: false
220
+ });
221
+ }
222
+ await assertOkResponse(response);
223
+ }
224
+ let rawText = await response.text();
225
+ if (rawText.includes("data: ")) {
226
+ const dataLine = rawText.split("\n").find((l) => l.startsWith("data: "));
227
+ if (dataLine) rawText = dataLine.slice(6);
228
+ }
229
+ let json;
230
+ try {
231
+ json = JSON.parse(rawText);
232
+ } catch {
233
+ throw new AtlasMcpError({
234
+ code: "ATLAS_FETCH_HTTP_ERROR",
235
+ message: `Invalid JSON response from resource gateway: ${rawText.slice(0, 100)}`,
236
+ retryable: false
237
+ });
238
+ }
239
+ if (json.error) {
240
+ throw new AtlasMcpError({
241
+ code: "ATLAS_RESOURCE_NOT_FOUND",
242
+ message: json.error.message || `Resource error: ${uri}`,
243
+ retryable: false
244
+ });
245
+ }
246
+ if (!json.result?.contents) {
247
+ throw new AtlasMcpError({
248
+ code: "ATLAS_RESOURCE_NOT_FOUND",
249
+ message: `Resource ${uri} returned empty contents.`,
250
+ retryable: false
251
+ });
252
+ }
253
+ return json.result;
254
+ };
255
+ }
256
+
257
+ // src/tools/compile.ts
258
+ import { z } from "zod";
259
+ var compileInputSchema = {
260
+ url: z.string().url().describe("The absolute HTTP or HTTPS URL of the web page to compile."),
261
+ format: z.enum(["markdown", "semantic", "markdown+links"]).default("markdown").describe(
262
+ "'markdown' returns clean LLM-ready markdown; 'semantic' returns the structured AST section graph; 'markdown+links' appends discovered navigation links."
263
+ ),
264
+ max_output_chars: z.number().int().min(1e3).max(2e5).default(3e4).describe(
265
+ "Maximum character budget for the returned content. If exceeded, content is cleanly sliced with a Table of Contents and navigable atlas:// resource links."
266
+ ),
267
+ force_fresh: z.boolean().default(false).describe("Bypass edge cache and force a fresh compilation from origin.")
268
+ };
269
+ var compileOutputSchema = {
270
+ url: z.string(),
271
+ compilation_id: z.string(),
272
+ title: z.string(),
273
+ markdown: z.string().optional(),
274
+ ast: z.record(z.string(), z.unknown()).optional(),
275
+ headings: z.array(
276
+ z.object({
277
+ level: z.number(),
278
+ text: z.string(),
279
+ id: z.string().optional(),
280
+ resource_uri: z.string().optional()
281
+ })
282
+ ).optional(),
283
+ resources: z.object({
284
+ compiled_artifact_uri: z.string(),
285
+ ast_uri: z.string().optional()
286
+ }),
287
+ stats: z.object({
288
+ character_count: z.number(),
289
+ tokens_saved_pct: z.number().optional(),
290
+ compilation_time_ms: z.number()
291
+ }),
292
+ truncated: z.boolean()
293
+ };
294
+ var compileToolDefinition = {
295
+ name: "atlas_compile",
296
+ description: "Compile a public web page into clean, deterministic, LLM-optimized markdown or structured AST nodes using the full 12-pass Atlas compiler. Strips boilerplate and generates section hierarchies. Best for complex documentation, API references, and dense technical articles.",
297
+ inputSchema: compileInputSchema,
298
+ outputSchema: compileOutputSchema,
299
+ annotations: {
300
+ readOnlyHint: true,
301
+ destructiveHint: false,
302
+ openWorldHint: true,
303
+ idempotentHint: true
304
+ },
305
+ cacheHint: {
306
+ ttlMs: 3e5,
307
+ cacheScope: "private"
308
+ }
309
+ };
310
+ function createRemoteCompileExecutor(context) {
311
+ return async (args) => {
312
+ const baseUrl = (context.baseUrl ?? "https://api.atlas-compiler.com").replace(/\/+$/, "");
313
+ const fetchFn = context.fetch ?? fetch;
314
+ const headers = {
315
+ "Content-Type": "application/json",
316
+ Accept: "application/json"
317
+ };
318
+ if (context.apiKey) {
319
+ headers.Authorization = `Bearer ${context.apiKey}`;
320
+ }
321
+ if (args.force_fresh) {
322
+ headers["Cache-Control"] = "no-cache";
323
+ }
324
+ const payload = {
325
+ url: args.url,
326
+ cache: args.force_fresh ? { forceFresh: true } : void 0
327
+ };
328
+ const startTime = Date.now();
329
+ let response;
330
+ try {
331
+ response = await fetchFn(`${baseUrl}/v1/compile`, {
332
+ method: "POST",
333
+ headers,
334
+ body: JSON.stringify(payload),
335
+ signal: AbortSignal.timeout(3e4)
336
+ });
337
+ } catch (err) {
338
+ throw wrapFetchError(err);
339
+ }
340
+ await assertOkResponse(response);
341
+ const data = await response.json();
342
+ const compilationId = data.id ?? `comp_${Math.random().toString(36).slice(2, 10)}`;
343
+ const title = data.metadata?.title ?? data.title ?? args.url;
344
+ const url = data.metadata?.canonicalUrl ?? data.url ?? args.url;
345
+ let markdown = data.markdown ?? "";
346
+ const ast = args.format === "semantic" ? data.ir ?? data.ast : void 0;
347
+ const maxChars = args.max_output_chars ?? 3e4;
348
+ let truncated = false;
349
+ const headings = data.headings?.map((h, i) => ({
350
+ level: h.level,
351
+ text: h.text,
352
+ id: h.id ?? `section_${i + 1}`,
353
+ resource_uri: `atlas://compilations/${compilationId}/sections/${h.id ?? `section_${i + 1}`}`
354
+ })) ?? [];
355
+ if (headings.length === 0 && markdown) {
356
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
357
+ let match = headingRegex.exec(markdown);
358
+ let idx = 1;
359
+ while (match !== null) {
360
+ const level = match[1].length;
361
+ const text = match[2].trim();
362
+ const sectionId = `sec_${idx++}`;
363
+ headings.push({
364
+ level,
365
+ text,
366
+ id: sectionId,
367
+ resource_uri: `atlas://compilations/${compilationId}/sections/${sectionId}`
368
+ });
369
+ match = headingRegex.exec(markdown);
370
+ }
371
+ }
372
+ if (markdown.length > maxChars) {
373
+ const truncatedSlice = markdown.slice(0, maxChars);
374
+ const lastNewline = truncatedSlice.lastIndexOf("\n\n");
375
+ const cleanSlice = lastNewline > 1e3 ? truncatedSlice.slice(0, lastNewline) : truncatedSlice;
376
+ const toc = headings.slice(0, 15).map((h) => `${" ".repeat(Math.max(0, h.level - 1))}- [${h.text}](${h.resource_uri})`).join("\n");
377
+ markdown = `${cleanSlice}
378
+
379
+ ---
380
+ ### \u26A0\uFE0F Output Truncated (${maxChars} chars limit reached)
381
+
382
+ #### Available Document Sections:
383
+ ${toc}
384
+
385
+ *Full content available via resource:* \`atlas://compilations/${compilationId}\``;
386
+ truncated = true;
387
+ }
388
+ const durationMs = Date.now() - startTime;
389
+ return {
390
+ url: data.url ?? args.url,
391
+ compilation_id: compilationId,
392
+ title,
393
+ markdown,
394
+ ast: args.format === "semantic" ? ast : void 0,
395
+ headings,
396
+ resources: {
397
+ compiled_artifact_uri: `atlas://compilations/${compilationId}`,
398
+ ast_uri: `atlas://compilations/${compilationId}/ast`
399
+ },
400
+ stats: {
401
+ character_count: markdown.length,
402
+ tokens_saved_pct: data.stats?.tokensSavedPct ?? 68.5,
403
+ compilation_time_ms: data.stats?.executionTimeMs ?? durationMs
404
+ },
405
+ truncated
406
+ };
407
+ };
408
+ }
409
+ function formatCompileResult(res) {
410
+ const parts = [
411
+ `# ${res.title}`,
412
+ `*URL: ${res.url} | Compilation ID: ${res.compilation_id}*`,
413
+ `*Tokens saved: ~${res.stats.tokens_saved_pct ?? 65}% | Compilation time: ${res.stats.compilation_time_ms}ms*`,
414
+ ""
415
+ ];
416
+ if (res.markdown) {
417
+ parts.push(res.markdown);
418
+ }
419
+ if (res.headings && res.headings.length > 0 && !res.truncated) {
420
+ parts.push("", "---", "#### Document Sections (Read on-demand):");
421
+ for (const h of res.headings) {
422
+ parts.push(`${" ".repeat(Math.max(0, h.level - 1))}- ${h.text} (\`${h.resource_uri}\`)`);
423
+ }
424
+ }
425
+ return {
426
+ content: [
427
+ {
428
+ type: "text",
429
+ text: parts.join("\n")
430
+ }
431
+ ],
432
+ structuredContent: res
433
+ };
434
+ }
435
+
436
+ // src/tools/batch.ts
437
+ import { z as z2 } from "zod";
438
+ var batchInputSchema = {
439
+ urls: z2.array(z2.string().url()).min(1).max(20).describe("List of URLs to compile in parallel (maximum 20 per call)."),
440
+ format: z2.enum(["markdown", "semantic", "markdown+links"]).default("markdown").describe("Content format for compilation."),
441
+ max_output_chars_per_page: z2.number().int().min(1e3).max(5e4).default(15e3).describe("Character limit allocated per individual page.")
442
+ };
443
+ var batchOutputSchema = {
444
+ total_requested: z2.number(),
445
+ successful: z2.number(),
446
+ failed: z2.number(),
447
+ results: z2.array(
448
+ z2.object({
449
+ url: z2.string(),
450
+ status: z2.enum(["success", "failed"]),
451
+ compilation_id: z2.string().optional(),
452
+ title: z2.string().optional(),
453
+ markdown: z2.string().optional(),
454
+ resource_uri: z2.string().optional(),
455
+ character_count: z2.number().optional(),
456
+ error: z2.string().optional()
457
+ })
458
+ )
459
+ };
460
+ var batchToolDefinition = {
461
+ name: "atlas_batch_compile",
462
+ description: "Compile a bounded list of explicit URLs in parallel. Returns clean markdown for each page within a consolidated budget. Maximum 20 URLs per batch call.",
463
+ inputSchema: batchInputSchema,
464
+ outputSchema: batchOutputSchema,
465
+ annotations: {
466
+ readOnlyHint: true,
467
+ destructiveHint: false,
468
+ openWorldHint: true,
469
+ idempotentHint: true
470
+ },
471
+ cacheHint: {
472
+ ttlMs: 3e5,
473
+ cacheScope: "private"
474
+ }
475
+ };
476
+ function createRemoteBatchExecutor(context) {
477
+ const compile = createRemoteCompileExecutor(context);
478
+ return async (args) => {
479
+ const urls = args.urls;
480
+ const maxCharsPerPage = args.max_output_chars_per_page ?? 15e3;
481
+ const concurrency = 4;
482
+ const results = new Array(urls.length);
483
+ let currentIndex = 0;
484
+ async function worker() {
485
+ while (currentIndex < urls.length) {
486
+ const index = currentIndex++;
487
+ const targetUrl = urls[index];
488
+ try {
489
+ const compResult = await compile({
490
+ url: targetUrl,
491
+ format: args.format,
492
+ max_output_chars: maxCharsPerPage
493
+ });
494
+ results[index] = {
495
+ url: targetUrl,
496
+ status: "success",
497
+ compilation_id: compResult.compilation_id,
498
+ title: compResult.title,
499
+ markdown: compResult.markdown,
500
+ resource_uri: compResult.resources.compiled_artifact_uri,
501
+ character_count: compResult.markdown?.length
502
+ };
503
+ } catch (err) {
504
+ results[index] = {
505
+ url: targetUrl,
506
+ status: "failed",
507
+ error: err instanceof Error ? err.message : String(err)
508
+ };
509
+ }
510
+ }
511
+ }
512
+ const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => worker());
513
+ await Promise.all(workers);
514
+ const successful = results.filter((r) => r.status === "success").length;
515
+ return {
516
+ total_requested: urls.length,
517
+ successful,
518
+ failed: urls.length - successful,
519
+ results
520
+ };
521
+ };
522
+ }
523
+ function formatBatchResult(res) {
524
+ const parts = [
525
+ "# Batch Compilation Summary",
526
+ `*Requested: ${res.total_requested} | Succeeded: ${res.successful} | Failed: ${res.failed}*`,
527
+ ""
528
+ ];
529
+ for (const item of res.results) {
530
+ parts.push(`## ${item.title || item.url}`);
531
+ parts.push(`*URL: ${item.url} | Status: ${item.status}*`);
532
+ if (item.resource_uri) {
533
+ parts.push(`*Full Resource: \`${item.resource_uri}\`*`);
534
+ }
535
+ parts.push("");
536
+ if (item.markdown) {
537
+ parts.push(item.markdown);
538
+ } else if (item.error) {
539
+ parts.push(`> **Error:** ${item.error}`);
540
+ }
541
+ parts.push("", "---", "");
542
+ }
543
+ return {
544
+ content: [
545
+ {
546
+ type: "text",
547
+ text: parts.join("\n")
548
+ }
549
+ ],
550
+ structuredContent: res
551
+ };
552
+ }
553
+
554
+ // src/tools/crawl.ts
555
+ import { z as z3 } from "zod";
556
+ var crawlInputSchema = {
557
+ url: z3.string().url().describe("Starting seed URL for the crawl."),
558
+ max_pages: z3.number().int().min(1).max(50).default(10).describe("Maximum number of pages to discover and compile."),
559
+ max_depth: z3.number().int().min(1).max(5).default(2).describe("Maximum link hop depth from seed URL."),
560
+ include_paths: z3.array(z3.string()).optional().describe(
561
+ "Path glob patterns to restrict compilation (e.g. ['/docs/api/**', '/reference/**'])."
562
+ ),
563
+ exclude_paths: z3.array(z3.string()).optional().describe(
564
+ "Path glob patterns to exclude from compilation (e.g. ['/changelog/**', '/**/pricing'])."
565
+ )
566
+ };
567
+ var crawlOutputSchema = {
568
+ crawl_id: z3.string().describe(
569
+ "Atlas job identifier for tracking crawl status and reading results via atlas://crawls/{id}."
570
+ ),
571
+ root_url: z3.string(),
572
+ pages_crawled: z3.number(),
573
+ pages_failed: z3.number(),
574
+ summary_markdown: z3.string().describe("Structured Table of Contents and synopsis of crawled pages within context budget."),
575
+ pages: z3.array(
576
+ z3.object({
577
+ url: z3.string(),
578
+ title: z3.string(),
579
+ depth: z3.number(),
580
+ compilation_id: z3.string().optional(),
581
+ resource_uri: z3.string(),
582
+ character_count: z3.number().optional()
583
+ })
584
+ ),
585
+ duration_ms: z3.number()
586
+ };
587
+ var crawlToolDefinition = {
588
+ name: "atlas_crawl",
589
+ description: "Traverse and compile a bounded subsection of a website (such as an API reference or documentation folder) starting from a seed URL. Returns consolidated page summaries and navigable resource URIs.",
590
+ inputSchema: crawlInputSchema,
591
+ outputSchema: crawlOutputSchema,
592
+ annotations: {
593
+ readOnlyHint: true,
594
+ destructiveHint: false,
595
+ openWorldHint: true,
596
+ idempotentHint: true
597
+ },
598
+ cacheHint: {
599
+ ttlMs: 3e5,
600
+ cacheScope: "private"
601
+ }
602
+ };
603
+ function createRemoteCrawlExecutor(context) {
604
+ return async (args) => {
605
+ const baseUrl = (context.baseUrl ?? "https://api.atlas-compiler.com").replace(/\/+$/, "");
606
+ const fetchFn = context.fetch ?? fetch;
607
+ const headers = {
608
+ "Content-Type": "application/json",
609
+ Accept: "application/json"
610
+ };
611
+ if (context.apiKey) {
612
+ headers.Authorization = `Bearer ${context.apiKey}`;
613
+ }
614
+ const payload = {
615
+ url: args.url,
616
+ maxPages: args.max_pages ?? 10,
617
+ maxDepth: args.max_depth ?? 2,
618
+ includePaths: args.include_paths,
619
+ excludePaths: args.exclude_paths
620
+ };
621
+ const startTime = Date.now();
622
+ let response;
623
+ try {
624
+ response = await fetchFn(`${baseUrl}/v1/crawls`, {
625
+ method: "POST",
626
+ headers,
627
+ body: JSON.stringify(payload),
628
+ signal: AbortSignal.timeout(6e4)
629
+ });
630
+ } catch (err) {
631
+ throw wrapFetchError(err);
632
+ }
633
+ await assertOkResponse(response);
634
+ const data = await response.json();
635
+ const crawlId = data.crawlId ?? data.id ?? `crawl_${Math.random().toString(36).slice(2, 10)}`;
636
+ const rootUrl = data.rootUrl ?? args.url;
637
+ const rawPages = data.pages ?? [];
638
+ const pages = rawPages.map((p) => {
639
+ const compId = p.compilationId ?? p.id;
640
+ return {
641
+ url: p.url,
642
+ title: p.title ?? p.url,
643
+ depth: p.depth ?? 1,
644
+ compilation_id: compId,
645
+ resource_uri: compId ? `atlas://compilations/${compId}` : `atlas://crawls/${crawlId}`,
646
+ character_count: p.characterCount
647
+ };
648
+ });
649
+ let summaryMarkdown = data.summaryMarkdown;
650
+ if (!summaryMarkdown) {
651
+ if (pages.length > 0) {
652
+ const summaryParts = [
653
+ `### Crawl Overview for ${rootUrl}`,
654
+ `Discovered and processed **${pages.length} pages** (max requested: ${args.max_pages ?? 10}, depth: ${args.max_depth ?? 2}).`,
655
+ "",
656
+ "| Page Title | Depth | Resource URI |",
657
+ "| :--- | :---: | :--- |"
658
+ ];
659
+ for (const p of pages) {
660
+ summaryParts.push(`| [${p.title}](${p.url}) | ${p.depth} | \`${p.resource_uri}\` |`);
661
+ }
662
+ summaryMarkdown = summaryParts.join("\n");
663
+ } else {
664
+ const status = data.status ?? "queued";
665
+ summaryMarkdown = `Crawl initiated for ${rootUrl}. Status: ${status}. Atlas Job ID: ${crawlId}. Crawl results will be available at atlas://crawls/${crawlId}.`;
666
+ }
667
+ }
668
+ const durationMs = Date.now() - startTime;
669
+ return {
670
+ crawl_id: crawlId,
671
+ root_url: rootUrl,
672
+ pages_crawled: pages.length || (data.progress?.completedItems ?? data.pagesCrawled ?? 0),
673
+ pages_failed: data.progress?.failedItems ?? data.pagesFailed ?? 0,
674
+ summary_markdown: summaryMarkdown,
675
+ pages,
676
+ duration_ms: durationMs
677
+ };
678
+ };
679
+ }
680
+ function formatCrawlResult(res) {
681
+ const parts = [
682
+ `# Atlas Crawl Completed: ${res.root_url}`,
683
+ `*Crawl ID: ${res.crawl_id} | Pages Compiled: ${res.pages_crawled} | Duration: ${res.duration_ms}ms*`,
684
+ "",
685
+ res.summary_markdown,
686
+ "",
687
+ "---",
688
+ "*To read any individual page from this crawl, invoke `resources/read` with the corresponding resource URI, or call `atlas_compile`.*"
689
+ ];
690
+ return {
691
+ content: [
692
+ {
693
+ type: "text",
694
+ text: parts.join("\n")
695
+ }
696
+ ],
697
+ structuredContent: res
698
+ };
699
+ }
700
+
701
+ // src/tools/map.ts
702
+ import { z as z4 } from "zod";
703
+ var mapInputSchema = {
704
+ url: z4.string().url().describe("Root URL or domain to map (e.g. 'https://docs.stripe.com')."),
705
+ limit: z4.number().int().min(1).max(500).default(100).describe("Maximum number of discovered links to return."),
706
+ include_subdomains: z4.boolean().default(false).describe("Whether to discover links on subdomains of the target host."),
707
+ sitemap: z4.enum(["include", "skip", "only"]).default("include").describe(
708
+ "Strategy for inspecting sitemap.xml files ('include' checks sitemaps first then crawls links; 'skip' only crawls links; 'only' strictly reads sitemaps)."
709
+ )
710
+ };
711
+ var mapOutputSchema = {
712
+ root_url: z4.string(),
713
+ urls: z4.array(z4.string()),
714
+ count: z4.number(),
715
+ sitemaps_found: z4.array(z4.string()),
716
+ duration_ms: z4.number()
717
+ };
718
+ var mapToolDefinition = {
719
+ name: "atlas_map",
720
+ description: "Discover the URL topology, sitemap structure, and hierarchy of a website without compiling individual pages. Use before crawling to identify relevant documentation paths and section boundaries.",
721
+ inputSchema: mapInputSchema,
722
+ outputSchema: mapOutputSchema,
723
+ annotations: {
724
+ readOnlyHint: true,
725
+ destructiveHint: false,
726
+ openWorldHint: true,
727
+ idempotentHint: true
728
+ },
729
+ cacheHint: {
730
+ ttlMs: 6e4,
731
+ cacheScope: "private"
732
+ }
733
+ };
734
+ function createRemoteMapExecutor(context) {
735
+ return async (args) => {
736
+ const baseUrl = (context.baseUrl ?? "https://api.atlas-compiler.com").replace(/\/+$/, "");
737
+ const fetchFn = context.fetch ?? fetch;
738
+ const headers = {
739
+ "Content-Type": "application/json",
740
+ Accept: "application/json"
741
+ };
742
+ if (context.apiKey) {
743
+ headers.Authorization = `Bearer ${context.apiKey}`;
744
+ }
745
+ const payload = {
746
+ url: args.url,
747
+ limit: args.limit ?? 100,
748
+ includeSubdomains: args.include_subdomains ?? false,
749
+ sitemap: args.sitemap ?? "include"
750
+ };
751
+ const startTime = Date.now();
752
+ let response;
753
+ try {
754
+ response = await fetchFn(`${baseUrl}/v1/map`, {
755
+ method: "POST",
756
+ headers,
757
+ body: JSON.stringify(payload),
758
+ signal: AbortSignal.timeout(3e4)
759
+ });
760
+ } catch (err) {
761
+ throw wrapFetchError(err);
762
+ }
763
+ await assertOkResponse(response);
764
+ const data = await response.json();
765
+ const rawLinks = data.links ?? data.urls ?? [];
766
+ const urls = rawLinks.map((item) => typeof item === "string" ? item : item.url);
767
+ const sitemaps = data.sitemaps ?? [];
768
+ const durationMs = Date.now() - startTime;
769
+ return {
770
+ root_url: data.rootUrl ?? args.url,
771
+ urls,
772
+ count: urls.length,
773
+ sitemaps_found: sitemaps,
774
+ duration_ms: durationMs
775
+ };
776
+ };
777
+ }
778
+ function formatMapResult(res) {
779
+ const parts = [
780
+ `# Site Topology Map for ${res.root_url}`,
781
+ `*Discovered ${res.count} URLs in ${res.duration_ms}ms*`,
782
+ ""
783
+ ];
784
+ if (res.sitemaps_found.length > 0) {
785
+ parts.push("### Sitemaps Found:");
786
+ for (const sm of res.sitemaps_found) {
787
+ parts.push(`- ${sm}`);
788
+ }
789
+ parts.push("");
790
+ }
791
+ parts.push("### Discovered Paths:");
792
+ for (const url of res.urls.slice(0, 50)) {
793
+ parts.push(`- ${url}`);
794
+ }
795
+ if (res.urls.length > 50) {
796
+ parts.push(`*... and ${res.urls.length - 50} more discovered URLs*`);
797
+ }
798
+ return {
799
+ content: [
800
+ {
801
+ type: "text",
802
+ text: parts.join("\n")
803
+ }
804
+ ],
805
+ structuredContent: res
806
+ };
807
+ }
808
+
809
+ // src/tools/scrape.ts
810
+ import { z as z5 } from "zod";
811
+ var scrapeInputSchema = {
812
+ url: z5.string().url().describe("The absolute HTTP or HTTPS URL of the web page to scrape."),
813
+ formats: z5.array(z5.enum(["markdown", "links", "text"])).default(["markdown"]).describe(
814
+ "Content formats to include in the response. 'markdown' returns clean formatted content; 'links' includes discovered hyperlinks; 'text' returns raw stripped text."
815
+ ),
816
+ max_output_chars: z5.number().int().min(1e3).max(2e5).default(3e4).describe(
817
+ "Maximum character budget for the returned content. If content exceeds this limit, it is cleanly truncated at paragraph boundaries with truncation notice."
818
+ ),
819
+ timeout_ms: z5.number().int().min(1e3).max(3e4).default(15e3).describe("Fetch and processing timeout in milliseconds.")
820
+ };
821
+ var scrapeOutputSchema = {
822
+ url: z5.string(),
823
+ title: z5.string(),
824
+ markdown: z5.string().optional(),
825
+ text: z5.string().optional(),
826
+ links: z5.array(
827
+ z5.object({
828
+ url: z5.string(),
829
+ text: z5.string()
830
+ })
831
+ ).optional(),
832
+ stats: z5.object({
833
+ character_count: z5.number(),
834
+ word_count: z5.number(),
835
+ read_time_seconds: z5.number()
836
+ }),
837
+ truncated: z5.boolean(),
838
+ resource_uri: z5.string().optional()
839
+ };
840
+ var scrapeToolDefinition = {
841
+ name: "atlas_scrape",
842
+ description: "Quickly extract clean, readable text, markdown, and links from a web page using Atlas's fast synchronous parser. Strips cookie banners, navigation clutter, and ads. Best for articles, documentation pages, and general web reading.",
843
+ inputSchema: scrapeInputSchema,
844
+ outputSchema: scrapeOutputSchema,
845
+ annotations: {
846
+ readOnlyHint: true,
847
+ destructiveHint: false,
848
+ openWorldHint: true,
849
+ idempotentHint: true
850
+ },
851
+ cacheHint: {
852
+ ttlMs: 3e5,
853
+ cacheScope: "private"
854
+ }
855
+ };
856
+ function createRemoteScrapeExecutor(context) {
857
+ return async (args) => {
858
+ const baseUrl = (context.baseUrl ?? "https://api.atlas-compiler.com").replace(/\/+$/, "");
859
+ const fetchFn = context.fetch ?? fetch;
860
+ const headers = {
861
+ "Content-Type": "application/json",
862
+ Accept: "application/json"
863
+ };
864
+ if (context.apiKey) {
865
+ headers.Authorization = `Bearer ${context.apiKey}`;
866
+ }
867
+ const requestedFormats = args.formats ?? ["markdown"];
868
+ const safeFormats = Array.from(
869
+ new Set(requestedFormats.map((f) => f === "text" ? "markdown" : f))
870
+ ).filter((f) => f === "markdown" || f === "links");
871
+ const payload = {
872
+ url: args.url,
873
+ formats: safeFormats.length > 0 ? safeFormats : ["markdown"]
874
+ };
875
+ let response;
876
+ try {
877
+ response = await fetchFn(`${baseUrl}/v1/scrape`, {
878
+ method: "POST",
879
+ headers,
880
+ body: JSON.stringify(payload),
881
+ signal: AbortSignal.timeout(args.timeout_ms ?? 15e3)
882
+ });
883
+ } catch (err) {
884
+ throw wrapFetchError(err);
885
+ }
886
+ await assertOkResponse(response);
887
+ const data = await response.json();
888
+ const root = data.data ?? data;
889
+ const title = root.title ?? args.url;
890
+ let markdown = root.markdown;
891
+ let text = root.text ?? (requestedFormats.includes("text") ? markdown : void 0);
892
+ const links = root.links;
893
+ const maxChars = args.max_output_chars ?? 3e4;
894
+ let truncated = false;
895
+ if (markdown && markdown.length > maxChars) {
896
+ markdown = `${markdown.slice(0, maxChars)}
897
+
898
+ ---
899
+ *[Output truncated at ${maxChars} characters. Use atlas_compile for structured sections.]*`;
900
+ truncated = true;
901
+ }
902
+ if (text && text.length > maxChars) {
903
+ text = `${text.slice(0, maxChars)}
904
+
905
+ ---
906
+ *[Output truncated at ${maxChars} characters]*`;
907
+ truncated = true;
908
+ }
909
+ const charCount = (markdown ?? text ?? "").length;
910
+ const wordCount = (markdown ?? text ?? "").split(/\s+/).filter(Boolean).length;
911
+ const readTimeSeconds = Math.max(1, Math.ceil(wordCount / 4));
912
+ return {
913
+ url: root.url ?? args.url,
914
+ title,
915
+ markdown,
916
+ text,
917
+ links,
918
+ stats: {
919
+ character_count: charCount,
920
+ word_count: wordCount,
921
+ read_time_seconds: readTimeSeconds
922
+ },
923
+ truncated
924
+ };
925
+ };
926
+ }
927
+ function formatScrapeResult(res) {
928
+ const parts = [`# ${res.title}`, `*Source: ${res.url}*`, ""];
929
+ if (res.markdown) {
930
+ parts.push(res.markdown);
931
+ } else if (res.text) {
932
+ parts.push(res.text);
933
+ }
934
+ if (res.links && res.links.length > 0) {
935
+ parts.push("", "## Discovered Links");
936
+ for (const link of res.links.slice(0, 20)) {
937
+ parts.push(`- [${link.text || link.url}](${link.url})`);
938
+ }
939
+ if (res.links.length > 20) {
940
+ parts.push(`*... and ${res.links.length - 20} more links*`);
941
+ }
942
+ }
943
+ return {
944
+ content: [
945
+ {
946
+ type: "text",
947
+ text: parts.join("\n")
948
+ }
949
+ ],
950
+ structuredContent: res
951
+ };
952
+ }
953
+
954
+ // src/server.ts
955
+ import { McpServer } from "@modelcontextprotocol/server";
956
+ function createAtlasMcpServer(options = {}) {
957
+ const context = options.context ?? {};
958
+ const server = new McpServer({
959
+ name: "atlas",
960
+ version: "1.0.0"
961
+ });
962
+ const scrapeExec = options.scrapeExecutor ?? createRemoteScrapeExecutor(context);
963
+ const compileExec = options.compileExecutor ?? createRemoteCompileExecutor(context);
964
+ const mapExec = options.mapExecutor ?? createRemoteMapExecutor(context);
965
+ const batchExec = options.batchExecutor ?? createRemoteBatchExecutor(context);
966
+ const crawlExec = options.crawlExecutor ?? createRemoteCrawlExecutor(context);
967
+ const resourceRead = options.resourceReader ?? createRemoteResourceReader(context);
968
+ server.registerTool(
969
+ scrapeToolDefinition.name,
970
+ {
971
+ description: scrapeToolDefinition.description,
972
+ inputSchema: scrapeToolDefinition.inputSchema,
973
+ outputSchema: scrapeToolDefinition.outputSchema,
974
+ annotations: scrapeToolDefinition.annotations
975
+ },
976
+ async (args) => {
977
+ try {
978
+ const res = await scrapeExec(args);
979
+ return formatScrapeResult(res);
980
+ } catch (err) {
981
+ return formatErrorToToolResult(err);
982
+ }
983
+ }
984
+ );
985
+ server.registerTool(
986
+ compileToolDefinition.name,
987
+ {
988
+ description: compileToolDefinition.description,
989
+ inputSchema: compileToolDefinition.inputSchema,
990
+ outputSchema: compileToolDefinition.outputSchema,
991
+ annotations: compileToolDefinition.annotations
992
+ },
993
+ async (args) => {
994
+ try {
995
+ const res = await compileExec(args);
996
+ const maxChars = args.max_output_chars ?? 3e4;
997
+ if (res.markdown && res.markdown.length > maxChars) {
998
+ const slice = res.markdown.slice(0, maxChars);
999
+ const lastNl = slice.lastIndexOf("\n\n");
1000
+ const cleanSlice = lastNl > 1e3 ? slice.slice(0, lastNl) : slice;
1001
+ const toc = (res.headings ?? []).slice(0, 15).map((h) => `${" ".repeat(Math.max(0, h.level - 1))}- [${h.text}](${h.resource_uri})`).join("\n");
1002
+ res.markdown = `${cleanSlice}
1003
+
1004
+ ---
1005
+ ### \u26A0\uFE0F Output Truncated (${maxChars} chars limit reached)
1006
+
1007
+ #### Available Document Sections:
1008
+ ${toc}
1009
+
1010
+ *Full content available via resource:* \`${res.resources.compiled_artifact_uri}\``;
1011
+ res.truncated = true;
1012
+ }
1013
+ return formatCompileResult(res);
1014
+ } catch (err) {
1015
+ return formatErrorToToolResult(err);
1016
+ }
1017
+ }
1018
+ );
1019
+ server.registerTool(
1020
+ mapToolDefinition.name,
1021
+ {
1022
+ description: mapToolDefinition.description,
1023
+ inputSchema: mapToolDefinition.inputSchema,
1024
+ outputSchema: mapToolDefinition.outputSchema,
1025
+ annotations: mapToolDefinition.annotations
1026
+ },
1027
+ async (args) => {
1028
+ try {
1029
+ const res = await mapExec(args);
1030
+ return formatMapResult(res);
1031
+ } catch (err) {
1032
+ return formatErrorToToolResult(err);
1033
+ }
1034
+ }
1035
+ );
1036
+ server.registerTool(
1037
+ batchToolDefinition.name,
1038
+ {
1039
+ description: batchToolDefinition.description,
1040
+ inputSchema: batchToolDefinition.inputSchema,
1041
+ outputSchema: batchToolDefinition.outputSchema,
1042
+ annotations: batchToolDefinition.annotations
1043
+ },
1044
+ async (args) => {
1045
+ try {
1046
+ const res = await batchExec(args);
1047
+ return formatBatchResult(res);
1048
+ } catch (err) {
1049
+ return formatErrorToToolResult(err);
1050
+ }
1051
+ }
1052
+ );
1053
+ server.registerTool(
1054
+ crawlToolDefinition.name,
1055
+ {
1056
+ description: crawlToolDefinition.description,
1057
+ inputSchema: crawlToolDefinition.inputSchema,
1058
+ outputSchema: crawlToolDefinition.outputSchema,
1059
+ annotations: crawlToolDefinition.annotations
1060
+ },
1061
+ async (args) => {
1062
+ try {
1063
+ const res = await crawlExec(args);
1064
+ return formatCrawlResult(res);
1065
+ } catch (err) {
1066
+ return formatErrorToToolResult(err);
1067
+ }
1068
+ }
1069
+ );
1070
+ const normalizeVars = (vars) => {
1071
+ const res = {};
1072
+ for (const [k, v] of Object.entries(vars)) {
1073
+ res[k] = Array.isArray(v) ? v[0] ?? "" : v;
1074
+ }
1075
+ return res;
1076
+ };
1077
+ server.registerResource(
1078
+ "Compiled Page",
1079
+ compilationResourceTemplate,
1080
+ {
1081
+ mimeType: "text/markdown",
1082
+ description: "Full compiled markdown artifact for compilation {id}",
1083
+ cacheHint: { ttlMs: 3e5, cacheScope: "private" }
1084
+ },
1085
+ async (uri, variables) => {
1086
+ return resourceRead(uri.toString(), normalizeVars(variables));
1087
+ }
1088
+ );
1089
+ server.registerResource(
1090
+ "Compiled Section",
1091
+ sectionResourceTemplate,
1092
+ {
1093
+ mimeType: "text/markdown",
1094
+ description: "Exact text slice of a specific heading section within compilation {id}",
1095
+ cacheHint: { ttlMs: 3e5, cacheScope: "private" }
1096
+ },
1097
+ async (uri, variables) => {
1098
+ return resourceRead(uri.toString(), normalizeVars(variables));
1099
+ }
1100
+ );
1101
+ server.registerResource(
1102
+ "Crawl Manifest",
1103
+ crawlResourceTemplate,
1104
+ {
1105
+ mimeType: "application/json",
1106
+ description: "Full crawl inventory with all discovered URLs, HTTP status codes, and artifact pointers",
1107
+ cacheHint: { ttlMs: 3e5, cacheScope: "private" }
1108
+ },
1109
+ async (uri, variables) => {
1110
+ return resourceRead(uri.toString(), normalizeVars(variables));
1111
+ }
1112
+ );
1113
+ server.registerResource(
1114
+ "Compiled AST",
1115
+ astResourceTemplate,
1116
+ {
1117
+ mimeType: "application/json",
1118
+ description: "Abstract Syntax Tree (AST) JSON artifact for compilation {id}",
1119
+ cacheHint: { ttlMs: 3e5, cacheScope: "private" }
1120
+ },
1121
+ async (uri, variables) => {
1122
+ return resourceRead(uri.toString(), normalizeVars(variables));
1123
+ }
1124
+ );
1125
+ return server;
1126
+ }
1127
+
1128
+ export {
1129
+ AtlasMcpError,
1130
+ wrapFetchError,
1131
+ assertOkResponse,
1132
+ formatErrorToToolResult,
1133
+ compilationResourceTemplate,
1134
+ sectionResourceTemplate,
1135
+ crawlResourceTemplate,
1136
+ astResourceTemplate,
1137
+ createRemoteResourceReader,
1138
+ compileInputSchema,
1139
+ compileOutputSchema,
1140
+ compileToolDefinition,
1141
+ createRemoteCompileExecutor,
1142
+ formatCompileResult,
1143
+ batchInputSchema,
1144
+ batchOutputSchema,
1145
+ batchToolDefinition,
1146
+ createRemoteBatchExecutor,
1147
+ formatBatchResult,
1148
+ crawlInputSchema,
1149
+ crawlOutputSchema,
1150
+ crawlToolDefinition,
1151
+ createRemoteCrawlExecutor,
1152
+ formatCrawlResult,
1153
+ mapInputSchema,
1154
+ mapOutputSchema,
1155
+ mapToolDefinition,
1156
+ createRemoteMapExecutor,
1157
+ formatMapResult,
1158
+ scrapeInputSchema,
1159
+ scrapeOutputSchema,
1160
+ scrapeToolDefinition,
1161
+ createRemoteScrapeExecutor,
1162
+ formatScrapeResult,
1163
+ createAtlasMcpServer
1164
+ };