amenbo 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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/adapters/index.d.ts +22 -0
  4. package/dist/adapters/index.js +51 -0
  5. package/dist/adapters/index.js.map +1 -0
  6. package/dist/adapters/types.d.ts +21 -0
  7. package/dist/adapters/types.js +2 -0
  8. package/dist/adapters/types.js.map +1 -0
  9. package/dist/cache.d.ts +90 -0
  10. package/dist/cache.js +286 -0
  11. package/dist/cache.js.map +1 -0
  12. package/dist/cli.d.ts +46 -0
  13. package/dist/cli.js +327 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/core.d.ts +78 -0
  16. package/dist/core.js +649 -0
  17. package/dist/core.js.map +1 -0
  18. package/dist/diff.d.ts +20 -0
  19. package/dist/diff.js +44 -0
  20. package/dist/diff.js.map +1 -0
  21. package/dist/errors.d.ts +97 -0
  22. package/dist/errors.js +209 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/extract/dataSources.d.ts +10 -0
  25. package/dist/extract/dataSources.js +115 -0
  26. package/dist/extract/dataSources.js.map +1 -0
  27. package/dist/extract/geometry.d.ts +63 -0
  28. package/dist/extract/geometry.js +77 -0
  29. package/dist/extract/geometry.js.map +1 -0
  30. package/dist/extract/markdown.d.ts +24 -0
  31. package/dist/extract/markdown.js +221 -0
  32. package/dist/extract/markdown.js.map +1 -0
  33. package/dist/extract/outline.d.ts +33 -0
  34. package/dist/extract/outline.js +128 -0
  35. package/dist/extract/outline.js.map +1 -0
  36. package/dist/extract/pdf.d.ts +22 -0
  37. package/dist/extract/pdf.js +95 -0
  38. package/dist/extract/pdf.js.map +1 -0
  39. package/dist/extract/preview.d.ts +19 -0
  40. package/dist/extract/preview.js +94 -0
  41. package/dist/extract/preview.js.map +1 -0
  42. package/dist/extract/pruning.d.ts +61 -0
  43. package/dist/extract/pruning.js +101 -0
  44. package/dist/extract/pruning.js.map +1 -0
  45. package/dist/extract/qualityScore.d.ts +67 -0
  46. package/dist/extract/qualityScore.js +98 -0
  47. package/dist/extract/qualityScore.js.map +1 -0
  48. package/dist/fetcher/browser.d.ts +85 -0
  49. package/dist/fetcher/browser.js +343 -0
  50. package/dist/fetcher/browser.js.map +1 -0
  51. package/dist/fetcher/http.d.ts +105 -0
  52. package/dist/fetcher/http.js +538 -0
  53. package/dist/fetcher/http.js.map +1 -0
  54. package/dist/fetcher/index.d.ts +41 -0
  55. package/dist/fetcher/index.js +92 -0
  56. package/dist/fetcher/index.js.map +1 -0
  57. package/dist/jp/consentBanner.d.ts +27 -0
  58. package/dist/jp/consentBanner.js +48 -0
  59. package/dist/jp/consentBanner.js.map +1 -0
  60. package/dist/jp/normalize.d.ts +30 -0
  61. package/dist/jp/normalize.js +65 -0
  62. package/dist/jp/normalize.js.map +1 -0
  63. package/dist/links.d.ts +20 -0
  64. package/dist/links.js +223 -0
  65. package/dist/links.js.map +1 -0
  66. package/dist/politeness.d.ts +66 -0
  67. package/dist/politeness.js +170 -0
  68. package/dist/politeness.js.map +1 -0
  69. package/dist/screenshot.d.ts +38 -0
  70. package/dist/screenshot.js +90 -0
  71. package/dist/screenshot.js.map +1 -0
  72. package/dist/server.d.ts +3 -0
  73. package/dist/server.js +111 -0
  74. package/dist/server.js.map +1 -0
  75. package/dist/templateLearning.d.ts +11 -0
  76. package/dist/templateLearning.js +42 -0
  77. package/dist/templateLearning.js.map +1 -0
  78. package/dist/tokens.d.ts +47 -0
  79. package/dist/tokens.js +141 -0
  80. package/dist/tokens.js.map +1 -0
  81. package/package.json +61 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * politeness.ts — 収集先への低負荷を担保するpolitenessマネージャ。
3
+ *
4
+ * - robots.txt尊重(robots-parser)。取得結果はプロセス内でキャッシュ(TTL付き)
5
+ * - Crawl-Delay尊重
6
+ * - ドメイン毎の直列キュー + 既定最小間隔1秒
7
+ */
8
+ import robotsParserImport from "robots-parser";
9
+ import { HttpStatusError, PrivateAddressError, RobotsDeniedError } from "./errors.js";
10
+ import { httpGet, USER_AGENT } from "./fetcher/http.js";
11
+ const robotsParser = robotsParserImport;
12
+ const DEFAULT_MIN_INTERVAL_MS = 1000;
13
+ const DEFAULT_ROBOTS_TTL_MS = 60 * 60 * 1000; // 1時間
14
+ const ROBOTS_FETCH_TIMEOUT_MS = 5000;
15
+ function defaultSleep(ms) {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+ export class PolitenessManager {
19
+ minIntervalMs;
20
+ robotsTtlMs;
21
+ now;
22
+ sleep;
23
+ store;
24
+ robotsCache = new Map();
25
+ // N8: settledを持たせるのは、pruneStaleHostStateがまだ完了していない(=キューに並んでいる
26
+ // 呼び出しがある)ロックを誤って消してしまうと直列化が壊れる(同一ホストへの同時呼び出しが
27
+ // 追い越してしまう)ため。「完了済み」かつ「最終アクセスから十分時間が経っている」ロックのみ
28
+ // 安全に削除できる。
29
+ locks = new Map();
30
+ lastRequestAt = new Map();
31
+ constructor(options = {}) {
32
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
33
+ this.robotsTtlMs = options.robotsTtlMs ?? DEFAULT_ROBOTS_TTL_MS;
34
+ this.now = options.now ?? Date.now;
35
+ this.sleep = options.sleep ?? defaultSleep;
36
+ this.store = options.store;
37
+ }
38
+ /** robots.txtとレート制御の両方を確認し、アクセス可能になるまで待機する。拒否時はRobotsDeniedErrorを投げる。 */
39
+ async guard(url) {
40
+ await this.checkRobotsAllowed(url);
41
+ await this.waitTurn(url);
42
+ }
43
+ /** robots.txtのみ確認する(waitTurnは行わない)。 */
44
+ async checkRobotsAllowed(url) {
45
+ const robots = await this.getRobots(url);
46
+ if (robots && robots.isAllowed(url, USER_AGENT) === false) {
47
+ throw new RobotsDeniedError(url);
48
+ }
49
+ }
50
+ /** robots.txtが宣言するSitemap URLの一覧を返す(linksツールのsitemap優先探索用)。 */
51
+ async getSitemaps(url) {
52
+ const robots = await this.getRobots(url);
53
+ return robots?.getSitemaps() ?? [];
54
+ }
55
+ /**
56
+ * ドメイン毎の直列キュー+最小間隔(robots.txtのCrawl-Delay考慮)を適用して順番を待つ。
57
+ * 直列キュー(locks)による直列化はプロセス内のみ有効。store(PolitenessStore)経由の
58
+ * プロセス間共有はread-modify-writeがアトミックではないベストエフォート(PolitenessStoreの
59
+ * JSDoc参照)。
60
+ */
61
+ async waitTurn(url) {
62
+ this.pruneStaleHostState();
63
+ const host = new URL(url).host;
64
+ const crawlDelayMs = await this.getCrawlDelayMs(url);
65
+ const intervalMs = Math.max(this.minIntervalMs, crawlDelayMs);
66
+ const previousEntry = this.locks.get(host);
67
+ const previous = previousEntry?.chain ?? Promise.resolve();
68
+ let release = () => { };
69
+ const ourTurn = new Promise((resolve) => {
70
+ release = resolve;
71
+ });
72
+ const entry = { chain: previous.then(() => ourTurn), settled: false };
73
+ this.locks.set(host, entry);
74
+ await previous;
75
+ try {
76
+ // CLI併設対応: インメモリのlastRequestAtとstore(プロセス間永続化)の値のうち新しい方を
77
+ // 採用する。store未指定時はstoreLastAtが常に-Infinityになり従来通りの挙動になる。
78
+ const memoryLastAt = this.lastRequestAt.get(host) ?? -Infinity;
79
+ const storeLastAt = this.store?.getLastRequestAt(host) ?? -Infinity;
80
+ const lastAt = Math.max(memoryLastAt, storeLastAt);
81
+ const elapsed = this.now() - lastAt;
82
+ if (elapsed < intervalMs) {
83
+ await this.sleep(intervalMs - elapsed);
84
+ }
85
+ const requestAt = this.now();
86
+ this.lastRequestAt.set(host, requestAt);
87
+ this.store?.setLastRequestAt(host, requestAt);
88
+ }
89
+ finally {
90
+ release();
91
+ entry.settled = true;
92
+ }
93
+ }
94
+ /**
95
+ * N8: robotsCache/locks/lastRequestAtは訪問したホスト毎に1エントリずつ増え、長時間稼働する
96
+ * MCPサーバープロセスでは無制限に増加しうる。robotsTtlMsを「エントリの生存期間」の目安として
97
+ * 転用し、しばらく使われていないホストの状態を掃除する。
98
+ *
99
+ * locksは、まだ完了していない(settled=false)呼び出しが並んでいる可能性があるホストを
100
+ * 誤って削除すると直列化が壊れる(新しい呼び出しが空のロックを掴んで先に進んでしまう)ため、
101
+ * settled=trueのエントリのみ削除する。
102
+ */
103
+ pruneStaleHostState() {
104
+ const cutoff = this.now() - this.robotsTtlMs;
105
+ for (const [host, lastAt] of this.lastRequestAt) {
106
+ if (lastAt >= cutoff)
107
+ continue;
108
+ const lock = this.locks.get(host);
109
+ if (!lock || lock.settled) {
110
+ this.lastRequestAt.delete(host);
111
+ this.locks.delete(host);
112
+ }
113
+ }
114
+ for (const [origin, entry] of this.robotsCache) {
115
+ if (entry.fetchedAt < cutoff) {
116
+ this.robotsCache.delete(origin);
117
+ }
118
+ }
119
+ }
120
+ async getCrawlDelayMs(url) {
121
+ const robots = await this.getRobots(url);
122
+ const delaySec = robots?.getCrawlDelay(USER_AGENT);
123
+ return typeof delaySec === "number" && Number.isFinite(delaySec) ? delaySec * 1000 : 0;
124
+ }
125
+ async getRobots(url) {
126
+ this.pruneStaleHostState();
127
+ const parsed = new URL(url);
128
+ // 公開品質バグ修正: http(s)以外(file:等)はorigin(host)を持たず、new URL(url).origin が
129
+ // "null" という文字列になる。素通りさせると `null/robots.txt` という不正なURLを
130
+ // 組み立ててしまい、httpGet内部のguardPublicAddress(new URL())が生のTypeErrorを投げ、
131
+ // それが下のcatchでAmenboErrorではないためstderrにスタックトレースを吐いていた。
132
+ // 呼び出し元(server.ts/links.ts)が事前にassertHttpSchemeでスキーム検証している想定だが、
133
+ // politeness.ts単体でも安全側に倒す(non-http(s)は「robots.txtの概念が無い」として扱う)。
134
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
135
+ return null;
136
+ }
137
+ const origin = parsed.origin;
138
+ const cached = this.robotsCache.get(origin);
139
+ if (cached && this.now() - cached.fetchedAt < this.robotsTtlMs) {
140
+ return cached.robots;
141
+ }
142
+ const robotsUrl = `${origin}/robots.txt`;
143
+ let body = "";
144
+ try {
145
+ const result = await httpGet(robotsUrl, { timeoutMs: ROBOTS_FETCH_TIMEOUT_MS });
146
+ body = result.status === 200 ? result.html : "";
147
+ }
148
+ catch (error) {
149
+ // M5: catchが広すぎるとSSRF拒否(PrivateAddressError)まで「robots.txtが取得できない
150
+ // だけ」として握りつぶしてしまう問題があったため、PrivateAddressErrorは再送出する。
151
+ //
152
+ // 公開品質バグ修正: 一方で当初の修正はAmenboError全般(HttpStatusError等)まで
153
+ // 再送出しており、robots.txtが存在しない(404)という極めて一般的なケースで
154
+ // 「robots.txtが無いだけの普通のサイト」への通常のfetchすら失敗するようになって
155
+ // いた(HttpStatusErrorは非2xx全般で投げられるため)。404/403/5xx等のHTTPエラーは
156
+ // robots.txt不在の慣習的な扱い(=制限なし)としてそのまま静かに継続する
157
+ // (ログも出さない。頻出するため公開ツールとしてノイズになる)。
158
+ if (error instanceof PrivateAddressError)
159
+ throw error;
160
+ if (!(error instanceof HttpStatusError)) {
161
+ console.error(`robots.txtの取得に失敗しました(制限なしとして扱います): ${robotsUrl}`, error);
162
+ }
163
+ body = "";
164
+ }
165
+ const robots = robotsParser(robotsUrl, body);
166
+ this.robotsCache.set(origin, { robots, fetchedAt: this.now() });
167
+ return robots;
168
+ }
169
+ }
170
+ //# sourceMappingURL=politeness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"politeness.js","sourceRoot":"","sources":["../src/politeness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ export interface TileGeometry {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export declare const DEFAULT_TILE_WIDTH = 1280;
8
+ export declare const DEFAULT_TILE_HEIGHT = 1080;
9
+ /**
10
+ * ページ寸法から、幅tileWidth・高さtileHeight毎のタイル矩形を計算する(純関数)。
11
+ * 上から順に敷き詰め、最後のタイルは端数の高さになる。MAX_TILES枚を超える分は切り捨てる。
12
+ */
13
+ export declare function computeTiles(pageWidth: number, pageHeight: number, tileWidth?: number, tileHeight?: number): TileGeometry[];
14
+ /** N7: computeTilesがMAX_TILES枚で切り捨てを行ったかどうかを判定する(純関数)。 */
15
+ export declare function isTileCaptureTruncated(pageHeight: number, tileCount: number, tileHeight?: number): boolean;
16
+ export interface ScreenshotOptions {
17
+ /** 既定true。falseの場合は最初のビューポート分(1タイル)のみ撮影する。 */
18
+ fullPage?: boolean;
19
+ /** タイル幅(px)。既定1280。 */
20
+ width?: number;
21
+ /** 解像度スケール(0.5〜1.0)。既定1.0。小さいほど画像サイズ(トークン)が減る。 */
22
+ scale?: number;
23
+ timeoutMs?: number;
24
+ }
25
+ export interface ScreenshotTile {
26
+ geometry: TileGeometry;
27
+ png: Buffer;
28
+ }
29
+ export interface CaptureResult {
30
+ finalUrl: string;
31
+ pageWidth: number;
32
+ pageHeight: number;
33
+ tiles: ScreenshotTile[];
34
+ /** N7: MAX_TILES枚を超えるページで切り捨てが発生した場合true。 */
35
+ truncated: boolean;
36
+ }
37
+ /** URLをブラウザでレンダリングし、タイル分割したPNGスクリーンショット群を撮影する。 */
38
+ export declare function captureTiledScreenshot(url: string, options?: ScreenshotOptions): Promise<CaptureResult>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * screenshot.ts — 品質スコア自動切替(mode: auto)/screenshotツール共通のタイル撮影。
3
+ *
4
+ * Playwright共有インスタンス(fetcher/browser.ts)を利用し、フルページのスクリーンショットを
5
+ * 1回だけ撮影し、@napi-rs/canvasで幅1280px×高さ約1080px毎のタイルへクロップして分割する
6
+ * (N2: 以前はタイル毎にfullPageスクリーンショットを撮り直しており無駄が大きかった)。
7
+ * scaleはPlaywrightのdeviceScaleFactorとして働き、1未満にすると出力解像度(=画像バイト数
8
+ * ≒ トークン消費量)を圧縮できるレバーになる。
9
+ */
10
+ import { createCanvas, loadImage } from "@napi-rs/canvas";
11
+ import { hideConsentBanners, openPageAndNavigate } from "./fetcher/browser.js";
12
+ import { USER_AGENT } from "./fetcher/http.js";
13
+ export const DEFAULT_TILE_WIDTH = 1280;
14
+ export const DEFAULT_TILE_HEIGHT = 1080;
15
+ /** 無限スクロール等で異常に長いページの撮影を打ち切る上限枚数(トークン予算保護)。 */
16
+ const MAX_TILES = 10;
17
+ /**
18
+ * ページ寸法から、幅tileWidth・高さtileHeight毎のタイル矩形を計算する(純関数)。
19
+ * 上から順に敷き詰め、最後のタイルは端数の高さになる。MAX_TILES枚を超える分は切り捨てる。
20
+ */
21
+ export function computeTiles(pageWidth, pageHeight, tileWidth = DEFAULT_TILE_WIDTH, tileHeight = DEFAULT_TILE_HEIGHT) {
22
+ const width = pageWidth > 0 ? Math.min(tileWidth, pageWidth) : tileWidth;
23
+ const totalHeight = Math.max(pageHeight, 1);
24
+ const tiles = [];
25
+ for (let y = 0; y < totalHeight && tiles.length < MAX_TILES; y += tileHeight) {
26
+ const height = Math.min(tileHeight, totalHeight - y);
27
+ tiles.push({ x: 0, y, width, height });
28
+ }
29
+ return tiles.length > 0 ? tiles : [{ x: 0, y: 0, width, height: Math.min(tileHeight, totalHeight) }];
30
+ }
31
+ /** N7: computeTilesがMAX_TILES枚で切り捨てを行ったかどうかを判定する(純関数)。 */
32
+ export function isTileCaptureTruncated(pageHeight, tileCount, tileHeight = DEFAULT_TILE_HEIGHT) {
33
+ const tilesNeeded = Math.ceil(Math.max(pageHeight, 1) / tileHeight);
34
+ return tilesNeeded > tileCount;
35
+ }
36
+ const DEFAULT_TIMEOUT_MS = 15_000;
37
+ const MIN_SCALE = 0.5;
38
+ const MAX_SCALE = 1.0;
39
+ function clampScale(scale) {
40
+ if (scale === undefined)
41
+ return MAX_SCALE;
42
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale));
43
+ }
44
+ /** URLをブラウザでレンダリングし、タイル分割したPNGスクリーンショット群を撮影する。 */
45
+ export async function captureTiledScreenshot(url, options = {}) {
46
+ const width = options.width ?? DEFAULT_TILE_WIDTH;
47
+ const scale = clampScale(options.scale);
48
+ const fullPage = options.fullPage ?? true;
49
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
50
+ // C1: SSRF/スキーム検証(guardPublicAddress)+リダイレクト再検証込みのナビゲーション。
51
+ // 改善キュー対応: 同梱ChromiumがERR_HTTP2_PROTOCOL_ERROR系で失敗した場合、システムに
52
+ // Chromeがあれば1回だけそちらへフォールバックする(openPageAndNavigate参照)。
53
+ const { context, page } = await openPageAndNavigate(url, timeoutMs, {
54
+ userAgent: USER_AGENT,
55
+ viewport: { width, height: DEFAULT_TILE_HEIGHT },
56
+ deviceScaleFactor: scale,
57
+ });
58
+ try {
59
+ // J8: 撮影前にCookie同意バナー/アプリ誘導オーバーレイを隠す(視覚的な妨げを除く)
60
+ await hideConsentBanners(page).catch(() => {
61
+ // ベストエフォート。失敗しても撮影自体は続行する
62
+ });
63
+ const dimensions = await page.evaluate(() => ({
64
+ width: Math.ceil(document.documentElement.scrollWidth),
65
+ height: Math.ceil(document.documentElement.scrollHeight),
66
+ }));
67
+ const captureHeight = fullPage ? dimensions.height : Math.min(dimensions.height, DEFAULT_TILE_HEIGHT);
68
+ const geometries = computeTiles(dimensions.width, captureHeight, width, DEFAULT_TILE_HEIGHT);
69
+ const truncated = isTileCaptureTruncated(captureHeight, geometries.length);
70
+ // N2: タイル毎にfullPageスクリーンショットを撮り直すのではなく、1回だけ撮影して
71
+ // @napi-rs/canvasでタイル領域をクロップする(deviceScaleFactor分、座標をscale倍する)。
72
+ const fullPagePng = await page.screenshot({ type: "png", fullPage: true });
73
+ const image = await loadImage(fullPagePng);
74
+ const tiles = geometries.map((geometry) => {
75
+ const sx = Math.min(Math.round(geometry.x * scale), Math.max(image.width - 1, 0));
76
+ const sy = Math.min(Math.round(geometry.y * scale), Math.max(image.height - 1, 0));
77
+ const sw = Math.max(1, Math.min(Math.round(geometry.width * scale), image.width - sx));
78
+ const sh = Math.max(1, Math.min(Math.round(geometry.height * scale), image.height - sy));
79
+ const canvas = createCanvas(sw, sh);
80
+ const ctx = canvas.getContext("2d");
81
+ ctx.drawImage(image, sx, sy, sw, sh, 0, 0, sw, sh);
82
+ return { geometry, png: canvas.toBuffer("image/png") };
83
+ });
84
+ return { finalUrl: page.url(), pageWidth: dimensions.width, pageHeight: dimensions.height, tiles, truncated };
85
+ }
86
+ finally {
87
+ await context.close();
88
+ }
89
+ }
90
+ //# sourceMappingURL=screenshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** MCP stdioサーバーを起動する。CLI(cli.ts)が引数なし/`serve`サブコマンド時に呼ぶ後方互換経路でもある。 */
3
+ export declare function runServer(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * server.ts — amenbo MCP stdioサーバー。
4
+ *
5
+ * ツール(3個で確定。plan.md §3-6のツール数最小化方針):
6
+ * - fetch: politeness → cache(fresh/revalidated/unchanged/diff/miss) → 二段フェッチ
7
+ * → J4 pruning・J7アダプタ・J8バナー除去 込みのMarkdown抽出 → mode別出力
8
+ * (markdown/outline/section/screenshot)。PDFはURL判定で別経路。
9
+ * - links: sitemap/RSS優先のリンク列挙。
10
+ * - screenshot: 明示的な視覚確認用のタイル分割スクリーンショット。
11
+ *
12
+ * ツール登録・エラーラッピング・stdioトランスポート接続のみを担当する。
13
+ * politeness/cacheのシングルトンやツールハンドラの実処理はcore.ts(CLIと共有)にある。
14
+ */
15
+ import { fileURLToPath } from "node:url";
16
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+ import { z } from "zod";
19
+ import { formatLinksResponse, handleFetchTool, handleScreenshotTool, politeness, resolvePackageVersion, } from "./core.js";
20
+ import { AmenboError } from "./errors.js";
21
+ import { discoverLinks } from "./links.js";
22
+ const server = new McpServer({ name: "amenbo", version: resolvePackageVersion() });
23
+ server.registerTool("fetch", {
24
+ title: "Fetch a web page as Markdown",
25
+ description: "日本語Webページを低負荷・省トークンで取得(robots.txt/レート制御/キャッシュ内蔵)。" +
26
+ "mode: auto(既定,品質スコアでMarkdown/screenshot自動切替)/markdown/outline(見出し要約)/screenshot。" +
27
+ "再取得時はcache: unchanged(無変更)/diff(変更節のみ)で省トークン応答。PDFはURLで自動判定。",
28
+ inputSchema: {
29
+ url: z.string().url().describe("取得対象URL(http/httpsのみ。PDFも可)"),
30
+ mode: z.enum(["auto", "markdown", "outline", "screenshot"]).optional().describe("既定auto"),
31
+ selector: z.string().optional().describe("本文を絞り込むCSSセレクタ"),
32
+ section: z.string().optional().describe("outlineで得たsection ID。指定時はその節のMarkdownのみ返す"),
33
+ page: z.number().int().positive().optional().describe("ページ番号(既定1)"),
34
+ max_tokens: z.number().int().positive().optional().describe("1ページの概算トークン上限(既定8000)"),
35
+ force_full: z.boolean().optional().describe("既定false。trueで差分応答(unchanged/diff)と定型ブロック除去を無効化し常に全文を返す"),
36
+ },
37
+ }, async ({ url, mode, selector, section, page, max_tokens: maxTokens, force_full: forceFull }) => {
38
+ try {
39
+ const content = await handleFetchTool({ url, mode, selector, section, page, max_tokens: maxTokens, force_full: forceFull });
40
+ return { content };
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof AmenboError ? error.message : `予期しないエラーが発生しました: ${String(error)}`;
44
+ if (!(error instanceof AmenboError)) {
45
+ console.error(error);
46
+ }
47
+ return { content: [{ type: "text", text: message }], isError: true };
48
+ }
49
+ });
50
+ server.registerTool("links", {
51
+ title: "List links from a page (sitemap/RSS-first)",
52
+ description: "sitemap.xml/RSS・Atomフィードがあれば優先し、無ければページ内リンクを抽出する低負荷なリンク列挙。",
53
+ inputSchema: {
54
+ url: z.string().url().describe("起点URL"),
55
+ filter: z.string().optional().describe("URL/リンクテキストの部分一致、または*を使ったglob"),
56
+ },
57
+ }, async ({ url, filter }) => {
58
+ try {
59
+ const result = await discoverLinks(url, politeness, filter ? { filter } : {});
60
+ return { content: [{ type: "text", text: formatLinksResponse(url, result) }] };
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof AmenboError ? error.message : `予期しないエラーが発生しました: ${String(error)}`;
64
+ if (!(error instanceof AmenboError)) {
65
+ console.error(error);
66
+ }
67
+ return { content: [{ type: "text", text: message }], isError: true };
68
+ }
69
+ });
70
+ server.registerTool("screenshot", {
71
+ title: "Capture a tiled screenshot of a web page",
72
+ description: "明示的な視覚確認用。Playwrightでページをレンダリングし、タイル分割したPNGスクリーンショットを返す。robots.txt/レート制御/キャッシュを内蔵する。",
73
+ inputSchema: {
74
+ url: z.string().url().describe("撮影対象のURL(http/httpsのみ)"),
75
+ fullPage: z.boolean().optional().describe("既定true。falseの場合は最初のビューポート分(1タイル)のみ撮影する"),
76
+ width: z.number().int().positive().optional().describe("タイル幅(px)。既定1280"),
77
+ scale: z.number().min(0.5).max(1.0).optional().describe("解像度スケール(0.5〜1.0、既定1.0)。小さいほど画像サイズ(トークン)が減る"),
78
+ },
79
+ }, async ({ url, fullPage, width, scale }) => {
80
+ try {
81
+ const content = await handleScreenshotTool({ url, fullPage, width, scale });
82
+ return { content };
83
+ }
84
+ catch (error) {
85
+ const message = error instanceof AmenboError ? error.message : `予期しないエラーが発生しました: ${String(error)}`;
86
+ if (!(error instanceof AmenboError)) {
87
+ console.error(error);
88
+ }
89
+ return { content: [{ type: "text", text: message }], isError: true };
90
+ }
91
+ });
92
+ /** MCP stdioサーバーを起動する。CLI(cli.ts)が引数なし/`serve`サブコマンド時に呼ぶ後方互換経路でもある。 */
93
+ export async function runServer() {
94
+ const transport = new StdioServerTransport();
95
+ await server.connect(transport);
96
+ }
97
+ /**
98
+ * このモジュールが直接実行された(node dist/server.js / tsx src/server.ts / binエントリ経由)場合のみ
99
+ * trueを返す。テスト(vitest)がformatHandoffResponse等のユニットテストのためにこのファイルを
100
+ * importした際、stdioトランスポート接続(runServer())が誤って走ってテストプロセスがハングするのを防ぐ。
101
+ */
102
+ function isDirectlyExecuted() {
103
+ return typeof process.argv[1] === "string" && fileURLToPath(import.meta.url) === process.argv[1];
104
+ }
105
+ if (isDirectlyExecuted()) {
106
+ runServer().catch((error) => {
107
+ console.error("amenboサーバーの起動に失敗しました:", error);
108
+ process.exit(1);
109
+ });
110
+ }
111
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** Markdownをブロック分割し、各ブロックの内容ハッシュ一覧を返す(重複ブロックは1回のみ)。 */
2
+ export declare function computeBlockHashes(markdown: string): string[];
3
+ export interface RemoveTemplateBlocksResult {
4
+ markdown: string;
5
+ removedCount: number;
6
+ }
7
+ /**
8
+ * 定型ブロック(templateHashesに含まれるハッシュを持つブロック)をMarkdownから除去する。
9
+ * templateHashesが空の場合は何もしない(呼び出し側で force_full 等により無効化するケース)。
10
+ */
11
+ export declare function removeTemplateBlocks(markdown: string, templateHashes: ReadonlySet<string>): RemoveTemplateBlocksResult;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * templateLearning.ts — Phase 4 テンプレート学習(同一ドメインの定型ブロック除去)。
3
+ *
4
+ * 同一ドメインの複数ページで共通して出現するブロック(段落/リスト単位。tokens.tsの
5
+ * ブロック分割を再利用)は、ヘッダ・フッタ・定型ナビ等の「そのページ固有ではない」
6
+ * 内容とみなし、表示用Markdownから取り除く。
7
+ *
8
+ * 判定方法(cache.ts側で保持): ドメイン毎に直近N ページ分のブロックハッシュ集合を記録し、
9
+ * 直近N ページ全てに出現したハッシュを定型ブロックとみなす(cache.tsの
10
+ * getTemplateBlockHashesを参照)。本モジュールはハッシュ計算とブロック除去の純関数のみを担う。
11
+ */
12
+ import { hashContent } from "./cache.js";
13
+ import { splitIntoBlocks } from "./tokens.js";
14
+ /** Markdownをブロック分割し、各ブロックの内容ハッシュ一覧を返す(重複ブロックは1回のみ)。 */
15
+ export function computeBlockHashes(markdown) {
16
+ const hashes = new Set();
17
+ for (const block of splitIntoBlocks(markdown)) {
18
+ hashes.add(hashContent(block.text.trim()));
19
+ }
20
+ return [...hashes];
21
+ }
22
+ /**
23
+ * 定型ブロック(templateHashesに含まれるハッシュを持つブロック)をMarkdownから除去する。
24
+ * templateHashesが空の場合は何もしない(呼び出し側で force_full 等により無効化するケース)。
25
+ */
26
+ export function removeTemplateBlocks(markdown, templateHashes) {
27
+ if (templateHashes.size === 0) {
28
+ return { markdown, removedCount: 0 };
29
+ }
30
+ const blocks = splitIntoBlocks(markdown);
31
+ const kept = [];
32
+ let removedCount = 0;
33
+ for (const block of blocks) {
34
+ if (templateHashes.has(hashContent(block.text.trim()))) {
35
+ removedCount++;
36
+ continue;
37
+ }
38
+ kept.push(block.text);
39
+ }
40
+ return { markdown: kept.join("\n\n"), removedCount };
41
+ }
42
+ //# sourceMappingURL=templateLearning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templateLearning.js","sourceRoot":"","sources":["../src/templateLearning.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,uDAAuD;AACvD,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9C,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;AACrB,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,cAAmC;IACxF,IAAI,cAAc,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IACvC,CAAC;IAED,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;YACvD,YAAY,EAAE,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;AACvD,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * J5: CJK対応トークン見積り。
3
+ *
4
+ * 文字クラス別係数(plan.md §2 J5):
5
+ * - ASCII等(半角英数記号): 1トークン ≒ 3.8文字
6
+ * - CJK(かな/カナ/漢字/全角記号等): 1文字 ≒ 0.9トークン
7
+ *
8
+ * 設計判断: plan.mdはASCII/CJKの二区分のみを定義しているため、絵文字・キリル文字等の
9
+ * その他多バイト文字はASCII側の係数で近似する(Phase 1のスコープでは十分な精度と判断)。
10
+ */
11
+ /** テキストの概算トークン数を返す(CJK文字クラス別係数)。 */
12
+ export declare function estimateTokens(text: string): number;
13
+ /** Markdownの1ブロック(見出し/段落/リスト項目/表/コードフェンス等)。 */
14
+ export interface Block {
15
+ text: string;
16
+ isHeading: boolean;
17
+ }
18
+ /**
19
+ * Markdownをブロック単位(空行区切り)に分割する。
20
+ * フェンスコードブロック(```)内の空行はブロック境界として扱わない。
21
+ * templateLearning.ts(Phase 4定型ブロック除去)からも再利用される共通処理。
22
+ */
23
+ export declare function splitIntoBlocks(markdown: string): Block[];
24
+ export interface PaginatedResult {
25
+ /** 要求されたページ(範囲外の場合は最寄りの有効ページへ丸められる)の本文。 */
26
+ content: string;
27
+ /** 実際に返したページ番号(1始まり)。 */
28
+ page: number;
29
+ /** 総ページ数。 */
30
+ totalPages: number;
31
+ /** このページの概算トークン数。 */
32
+ tokens: number;
33
+ /** N6: 単一ブロックがmax_tokens予算を超過しており、分割できずそのまま返した場合true。 */
34
+ exceededBudget: boolean;
35
+ }
36
+ /**
37
+ * J5: max_tokens予算で見出し/段落境界を優先してページ分割する。
38
+ *
39
+ * アルゴリズム:
40
+ * 1. 空行(フェンスコード内を除く)でMarkdownをブロックに分割
41
+ * 2. ブロックを先頭から貪欲に詰め、予算超過前に新しいページへ切り替える
42
+ * 3. 見出しブロックの手前では、既に予算の60%以上を使っていれば先に改ページする
43
+ * (見出しがページ末尾に孤立するのを避けるため)
44
+ * 4. 単一ブロックが予算を超える場合(巨大な表/コード等)は、表・コードを
45
+ * 壊さないためブロックを割らずそのページ単独で返す
46
+ */
47
+ export declare function paginateMarkdown(markdown: string, maxTokens: number, page: number): PaginatedResult;