@goflag/mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/{chunk-5KYL7Z7T.js → chunk-NW2SIYA2.js} +69 -7
- package/dist/chunk-NW2SIYA2.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +34 -4
- package/dist/index.js +5 -3
- package/package.json +2 -2
- package/dist/chunk-5KYL7Z7T.js.map +0 -1
package/README.md
CHANGED
|
@@ -39,11 +39,24 @@ All four are read-only: none writes to disk or to the site.
|
|
|
39
39
|
|
|
40
40
|
## What it does not do
|
|
41
41
|
|
|
42
|
-
- **No
|
|
42
|
+
- **No header as a tool argument.** A token passed there would sit in the agent's transcript. A protected preview is configured instead — see below.
|
|
43
43
|
- **No command to run.** The agent starts its own dev server; this server never executes one.
|
|
44
44
|
- **No baseline written.** Capture one with `goflag <url> --update-baseline --baseline .goflag/baseline.json`; `compare_to_baseline` only reads it, and refuses a file that is not a goflag report without quoting it.
|
|
45
45
|
- **No remote mode.** It runs where the agent runs — which is why it can see `localhost`.
|
|
46
46
|
|
|
47
|
+
## A protected preview
|
|
48
|
+
|
|
49
|
+
Give the server the header and the origin it is for, in its environment:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
claude mcp add goflag \
|
|
53
|
+
-e GOFLAG_HEADERS="x-vercel-protection-bypass: <secret>" \
|
|
54
|
+
-e GOFLAG_HEADERS_ORIGIN=https://my-app-git-main-my-team.vercel.app \
|
|
55
|
+
-- npx -y @goflag/mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`GOFLAG_HEADERS` holds one `Name: value` per line; `GOFLAG_HEADERS_ORIGIN` the origins they are for, comma-separated. They are sent to those origins only, whatever URL the agent audits, and a value never appears in a result. A configuration the server cannot honour stops it from starting. [More on goflag.tech](https://goflag.tech/docs/mcp#a-protected-preview).
|
|
59
|
+
|
|
47
60
|
## Requirements
|
|
48
61
|
|
|
49
62
|
Node 22 or later. A page that renders in the browser is audited in Chromium, through the `playwright` package; without it, or with `static: true`, the server-rendered HTML is judged instead.
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
diffExitCode,
|
|
9
9
|
diffReports,
|
|
10
10
|
exitCode,
|
|
11
|
+
parseHeaderArg,
|
|
11
12
|
runAudit,
|
|
12
13
|
summarize,
|
|
13
14
|
totalFindings
|
|
@@ -16,7 +17,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
|
|
18
19
|
// package.json
|
|
19
|
-
var version = "0.1.
|
|
20
|
+
var version = "0.1.2";
|
|
20
21
|
|
|
21
22
|
// src/server.ts
|
|
22
23
|
var RULE_DOCS = "https://goflag.tech/docs/rules/";
|
|
@@ -27,6 +28,61 @@ var INSTRUCTIONS = `goflag audits a website's SEO and i18n plumbing: hreflang cl
|
|
|
27
28
|
Use it on the site you are editing, before committing a change to metadata, the sitemap, robots.txt or hreflang: start the dev server, call audit_site on its URL, fix what is new, and call it again. explain_rule gives a rule's requirement, how authoritative it is, and the documents behind it. compare_to_baseline answers "did my change make it worse?" against a report saved with the goflag CLI.
|
|
28
29
|
|
|
29
30
|
Findings carry a rigor: spec-required and vendor-spec are requirements, guideline and heuristic are advice.`;
|
|
31
|
+
function readConfiguredHeaders(env) {
|
|
32
|
+
const raw = env.GOFLAG_HEADERS?.trim();
|
|
33
|
+
if (!raw) return void 0;
|
|
34
|
+
const declared = (env.GOFLAG_HEADERS_ORIGIN ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
35
|
+
if (declared.length === 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"GOFLAG_HEADERS is set without GOFLAG_HEADERS_ORIGIN: name the origin the headers are for, e.g. https://preview.example.com."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const origins = /* @__PURE__ */ new Set();
|
|
41
|
+
for (const entry of declared) {
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(entry);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`GOFLAG_HEADERS_ORIGIN: ${entry} is not an origin.`);
|
|
47
|
+
}
|
|
48
|
+
const bare = url.pathname === "/" && !url.search && !url.hash && !url.username && !url.password;
|
|
49
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || !bare) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`GOFLAG_HEADERS_ORIGIN: ${entry} is not an origin \u2014 scheme and host only, e.g. https://preview.example.com.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
origins.add(url.origin);
|
|
55
|
+
}
|
|
56
|
+
const headers = {};
|
|
57
|
+
raw.split(/\r?\n/).forEach((line, index) => {
|
|
58
|
+
if (!line.trim()) return;
|
|
59
|
+
try {
|
|
60
|
+
const { name, value } = parseHeaderArg(line);
|
|
61
|
+
headers[name] = value;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`GOFLAG_HEADERS line ${index + 1}: ${err.message.replace(/^--header /, "")}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return { origins, headers };
|
|
69
|
+
}
|
|
70
|
+
function headersFor(url, configured) {
|
|
71
|
+
if (!configured) return void 0;
|
|
72
|
+
try {
|
|
73
|
+
return configured.origins.has(new URL(url).origin) ? { ...configured.headers } : void 0;
|
|
74
|
+
} catch {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function instructionsFor(configured) {
|
|
79
|
+
if (!configured) return INSTRUCTIONS;
|
|
80
|
+
const origins = [...configured.origins].join(", ");
|
|
81
|
+
const names = Object.keys(configured.headers).join(", ");
|
|
82
|
+
return `${INSTRUCTIONS}
|
|
83
|
+
|
|
84
|
+
This server was configured with request headers (${names}) for ${origins}: an audit of that origin sends them, so a protected preview there can be audited. You cannot set headers yourself, and they are never sent to another origin.`;
|
|
85
|
+
}
|
|
30
86
|
var auditShape = {
|
|
31
87
|
url: z.string().describe("The site to audit, http or https. A dev server's URL works: http://localhost:3000."),
|
|
32
88
|
maxPages: z.number().int().min(1).max(500).optional().describe(`Page budget. Default ${DEFAULT_MAX_PAGES}.`),
|
|
@@ -38,8 +94,9 @@ var auditShape = {
|
|
|
38
94
|
),
|
|
39
95
|
checkExternal: z.boolean().optional().describe("Also probe links to other sites. Off by default: slow, and not your site's fault.")
|
|
40
96
|
};
|
|
41
|
-
function auditOptions(args, extra) {
|
|
97
|
+
function auditOptions(args, extra, configured) {
|
|
42
98
|
return {
|
|
99
|
+
headers: headersFor(args.url, configured),
|
|
43
100
|
maxPages: args.maxPages ?? DEFAULT_MAX_PAGES,
|
|
44
101
|
locales: args.locales,
|
|
45
102
|
expectIndexing: args.expectIndexing,
|
|
@@ -115,9 +172,13 @@ function ruleView(rule, sources) {
|
|
|
115
172
|
docs: `${RULE_DOCS}${rule.id}`
|
|
116
173
|
};
|
|
117
174
|
}
|
|
118
|
-
function createGoflagServer() {
|
|
175
|
+
function createGoflagServer(options = {}) {
|
|
176
|
+
const configured = readConfiguredHeaders(options.env ?? process.env);
|
|
119
177
|
const catalog = buildRuleCatalog();
|
|
120
|
-
const server = new McpServer(
|
|
178
|
+
const server = new McpServer(
|
|
179
|
+
{ name: "goflag", version },
|
|
180
|
+
{ instructions: instructionsFor(configured) }
|
|
181
|
+
);
|
|
121
182
|
server.registerTool(
|
|
122
183
|
"audit_site",
|
|
123
184
|
{
|
|
@@ -128,7 +189,7 @@ function createGoflagServer() {
|
|
|
128
189
|
},
|
|
129
190
|
async (args, extra) => {
|
|
130
191
|
try {
|
|
131
|
-
const report = await runAudit(args.url, auditOptions(args, extra));
|
|
192
|
+
const report = await runAudit(args.url, auditOptions(args, extra, configured));
|
|
132
193
|
return ok(headline(report), {
|
|
133
194
|
exitCode: exitCode(report),
|
|
134
195
|
coverage: report.diagnostics.coverage ?? null,
|
|
@@ -154,7 +215,7 @@ function createGoflagServer() {
|
|
|
154
215
|
const baseline = await readBaseline(path);
|
|
155
216
|
if (typeof baseline === "string") return failure(baseline);
|
|
156
217
|
try {
|
|
157
|
-
const report = await runAudit(args.url, auditOptions(args, extra));
|
|
218
|
+
const report = await runAudit(args.url, auditOptions(args, extra, configured));
|
|
158
219
|
const diff = diffReports(baseline, report);
|
|
159
220
|
return ok(
|
|
160
221
|
`${headline(report)} Against the baseline: ${diff.added.length} new, ${diff.resolved.length} resolved, ${diff.unchanged} unchanged.`,
|
|
@@ -220,6 +281,7 @@ function createGoflagServer() {
|
|
|
220
281
|
}
|
|
221
282
|
|
|
222
283
|
export {
|
|
284
|
+
readConfiguredHeaders,
|
|
223
285
|
createGoflagServer
|
|
224
286
|
};
|
|
225
|
-
//# sourceMappingURL=chunk-
|
|
287
|
+
//# sourceMappingURL=chunk-NW2SIYA2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../package.json"],"sourcesContent":["/**\n * The goflag MCP server: four read-only tools over the CLI's own engine.\n *\n * Local on purpose (`docs/integrations-plan.md` D1). An MCP server that runs\n * somewhere else cannot see `localhost:3000`, nor a preview behind a company\n * VPN; this one runs where the agent runs and sees what it sees. It is also\n * thin on purpose: `runAudit`, `summarize`, `diffReports` and the rule\n * catalogue are the CLI's, imported by name, so an answer here is the answer\n * `goflag` would print — never a second opinion that drifts from it.\n *\n * What it deliberately does not offer:\n *\n * - No `start`. Letting an agent pass a command for this process to run is an\n * arbitrary-execution door; the agent starts its own dev server.\n * - No header as a tool argument. A value passed there would sit in the\n * agent's transcript. Headers come from the server's own configuration —\n * `GOFLAG_HEADERS`, bound to the origins in `GOFLAG_HEADERS_ORIGIN` — and\n * go to those origins only (`readConfiguredHeaders`).\n * - No write. A baseline is captured with `goflag --update-baseline`; this\n * server only reads one.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { extname } from \"node:path\";\n\nimport {\n buildRuleCatalog,\n diffExitCode,\n diffReports,\n exitCode,\n parseHeaderArg,\n runAudit,\n summarize,\n totalFindings,\n type AuditOptions,\n type CatalogRule,\n type GoflagReport,\n type ProgressEvent,\n} from \"@goflag/cli\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { RequestHandlerExtra } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport type {\n CallToolResult,\n ServerNotification,\n ServerRequest,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { version } from \"../package.json\";\n\n/** Where a rule explains itself, for a human the agent hands a link to. */\nconst RULE_DOCS = \"https://goflag.tech/docs/rules/\";\n\n/**\n * The page budget when the agent sets none. The CLI's is 200, sized for a CI\n * job that has minutes; an agent waits on the answer, and on openfinanceguide\n * the link check alone took 618 s of a 715 s run (0.2.15). An agent can ask for\n * more; a first call should come back fast.\n */\nconst DEFAULT_MAX_PAGES = 50;\n\n/** One progress notification per this many milliseconds at most, phase changes aside. */\nconst PROGRESS_INTERVAL_MS = 500;\n\nconst INSTRUCTIONS = `goflag audits a website's SEO and i18n plumbing: hreflang clusters, translation holes, sitemap and robots.txt agreement, canonical, Open Graph, icons, llms.txt, soft 404s.\n\nUse it on the site you are editing, before committing a change to metadata, the sitemap, robots.txt or hreflang: start the dev server, call audit_site on its URL, fix what is new, and call it again. explain_rule gives a rule's requirement, how authoritative it is, and the documents behind it. compare_to_baseline answers \"did my change make it worse?\" against a report saved with the goflag CLI.\n\nFindings carry a rigor: spec-required and vendor-spec are requirements, guideline and heuristic are advice.`;\n\n/** What the server reads at start, beside the protocol. */\nexport interface GoflagServerOptions {\n /** Where `GOFLAG_HEADERS` and `GOFLAG_HEADERS_ORIGIN` are read. Defaults to `process.env`. */\n env?: Readonly<Record<string, string | undefined>>;\n}\n\n/** Request headers from the server's configuration, and the origins they are for. */\nexport interface ConfiguredHeaders {\n origins: ReadonlySet<string>;\n headers: Readonly<Record<string, string>>;\n}\n\n/**\n * The headers a protected preview needs, read from the server's environment.\n *\n * GOFLAG_HEADERS one `Name: value` per line, as `--header` takes it\n * GOFLAG_HEADERS_ORIGIN the origins they are for, comma-separated\n *\n * Bound to origins because here, unlike on a command line, the URL is chosen\n * by the agent, call by call. Sent wherever it pointed, a preview's token would\n * go to any site a prompt injection named; bound, it goes to the origins the\n * person configured and nowhere else, which is the CLI's own rule — the header\n * goes to the origin the operator named — kept when the operator is not the one\n * typing the URL.\n *\n * A configuration that cannot be honoured stops the server rather than\n * running without it: headers with no origin, an origin that is not one, a line\n * `parseHeaderArg` refuses. Errors name the header and never quote its value.\n */\nexport function readConfiguredHeaders(\n env: Readonly<Record<string, string | undefined>>,\n): ConfiguredHeaders | undefined {\n const raw = env.GOFLAG_HEADERS?.trim();\n if (!raw) return undefined;\n\n const declared = (env.GOFLAG_HEADERS_ORIGIN ?? \"\")\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n if (declared.length === 0) {\n throw new Error(\n \"GOFLAG_HEADERS is set without GOFLAG_HEADERS_ORIGIN: name the origin the headers are for, e.g. https://preview.example.com.\",\n );\n }\n const origins = new Set<string>();\n for (const entry of declared) {\n let url: URL;\n try {\n url = new URL(entry);\n } catch {\n throw new Error(`GOFLAG_HEADERS_ORIGIN: ${entry} is not an origin.`);\n }\n const bare = url.pathname === \"/\" && !url.search && !url.hash && !url.username && !url.password;\n if ((url.protocol !== \"http:\" && url.protocol !== \"https:\") || !bare) {\n throw new Error(\n `GOFLAG_HEADERS_ORIGIN: ${entry} is not an origin — scheme and host only, e.g. https://preview.example.com.`,\n );\n }\n origins.add(url.origin);\n }\n\n const headers: Record<string, string> = {};\n raw.split(/\\r?\\n/).forEach((line, index) => {\n if (!line.trim()) return;\n try {\n const { name, value } = parseHeaderArg(line);\n headers[name] = value;\n } catch (err) {\n throw new Error(\n `GOFLAG_HEADERS line ${index + 1}: ${(err as Error).message.replace(/^--header /, \"\")}`,\n );\n }\n });\n return { origins, headers };\n}\n\n/** The headers for one audit: the configured ones, if `url` is on a configured origin. */\nfunction headersFor(url: string, configured: ConfiguredHeaders | undefined) {\n if (!configured) return undefined;\n try {\n return configured.origins.has(new URL(url).origin) ? { ...configured.headers } : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The instructions, plus the origins audited with configured headers, when there are any. */\nfunction instructionsFor(configured: ConfiguredHeaders | undefined): string {\n if (!configured) return INSTRUCTIONS;\n const origins = [...configured.origins].join(\", \");\n const names = Object.keys(configured.headers).join(\", \");\n return `${INSTRUCTIONS}\n\nThis server was configured with request headers (${names}) for ${origins}: an audit of that origin sends them, so a protected preview there can be audited. You cannot set headers yourself, and they are never sent to another origin.`;\n}\n\nconst auditShape = {\n url: z\n .string()\n .describe(\"The site to audit, http or https. A dev server's URL works: http://localhost:3000.\"),\n maxPages: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(`Page budget. Default ${DEFAULT_MAX_PAGES}.`),\n locales: z\n .array(z.string())\n .optional()\n .describe('Locales the site serves, e.g. [\"en\", \"fr\"], when its sitemap does not say.'),\n expectIndexing: z\n .enum([\"no\", \"yes\"])\n .optional()\n .describe('\"no\" for a staging or preview host that must stay out of search indexes.'),\n static: z\n .boolean()\n .optional()\n .describe(\"Never launch Chromium; judge the server-rendered HTML only.\"),\n advisories: z\n .boolean()\n .optional()\n .describe(\n \"Also return the questions a linter cannot settle, with their evidence, for you to judge.\",\n ),\n checkExternal: z\n .boolean()\n .optional()\n .describe(\"Also probe links to other sites. Off by default: slow, and not your site's fault.\"),\n};\n\ntype AuditArgs = {\n url: string;\n maxPages?: number;\n locales?: string[];\n expectIndexing?: \"no\" | \"yes\";\n static?: boolean;\n advisories?: boolean;\n checkExternal?: boolean;\n};\n\ntype ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;\n\nfunction auditOptions(\n args: AuditArgs,\n extra: ToolExtra,\n configured: ConfiguredHeaders | undefined,\n): AuditOptions {\n return {\n headers: headersFor(args.url, configured),\n maxPages: args.maxPages ?? DEFAULT_MAX_PAGES,\n locales: args.locales,\n expectIndexing: args.expectIndexing,\n static: args.static,\n advisories: args.advisories,\n checkExternal: args.checkExternal ?? false,\n signal: extra.signal,\n onProgress: progressReporter(extra),\n };\n}\n\n/**\n * Crawl progress as MCP progress notifications, when the client asked for them\n * (a `progressToken`). Throttled: a crawl reports every page, and a client\n * drawing a spinner needs a heartbeat, not a stream. `progress` only grows, as\n * the protocol requires; the crawl's own total grows as links are found, so it\n * goes in the message rather than in `total`.\n */\nfunction progressReporter(extra: ToolExtra): ((event: ProgressEvent) => void) | undefined {\n const token = extra._meta?.progressToken;\n if (token === undefined) return undefined;\n let progress = 0;\n let lastPhase = \"\";\n let lastSent = 0;\n return (event) => {\n progress += 1;\n const now = Date.now();\n if (event.phase === lastPhase && now - lastSent < PROGRESS_INTERVAL_MS) return;\n lastPhase = event.phase;\n lastSent = now;\n extra\n .sendNotification({\n method: \"notifications/progress\",\n params: {\n progressToken: token,\n progress,\n message: `${event.phase}: ${event.done} of ${event.total}`,\n },\n })\n .catch(() => undefined);\n };\n}\n\nfunction ok(text: string, structured: Record<string, unknown>): CallToolResult {\n return {\n content: [{ type: \"text\", text: `${text}\\n\\n${JSON.stringify(structured)}` }],\n structuredContent: structured,\n };\n}\n\nfunction failure(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/** One line a human can read before the JSON an agent reads. */\nfunction headline(report: GoflagReport): string {\n const total = totalFindings(report);\n const coverage = report.diagnostics.coverage;\n const pages =\n coverage?.selected !== undefined && coverage.considered !== undefined\n ? `${coverage.selected} of ${coverage.considered} sitemap URLs`\n : `${report.diagnostics.pagesCrawled} pages`;\n return `${report.url}: ${report.summary.verdict} flag, ${total} finding${total === 1 ? \"\" : \"s\"}, ${pages} audited.`;\n}\n\n/**\n * Read a report saved by the CLI. Errors say what was wrong with the file and\n * never quote it: an agent can point this at any path, and a parse error that\n * echoes the first bytes of `~/.ssh/id_ed25519` into the transcript would make\n * a read-only tool a way to read files the agent was not shown.\n */\nasync function readBaseline(path: string): Promise<GoflagReport | string> {\n if (extname(path).toLowerCase() !== \".json\") {\n return `${path} is not a .json file. Pass a report saved by goflag --report or --update-baseline.`;\n }\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n return `Could not read ${path}.`;\n }\n try {\n const parsed = JSON.parse(raw) as Partial<GoflagReport>;\n if (typeof parsed.url === \"string\" && parsed.summary && Array.isArray(parsed.seoIssues)) {\n return parsed as GoflagReport;\n }\n } catch {\n // Falls through to the same answer as a JSON file of the wrong shape.\n }\n return `${path} is not a goflag JSON report.`;\n}\n\nfunction ruleView(rule: CatalogRule, sources: ReturnType<typeof buildRuleCatalog>[\"sources\"]) {\n return {\n ...rule,\n sources: rule.sources.map((id) => sources[id] ?? { id }),\n docs: `${RULE_DOCS}${rule.id}`,\n };\n}\n\n/** A server with goflag's four tools, not yet connected to a transport. */\nexport function createGoflagServer(options: GoflagServerOptions = {}): McpServer {\n const configured = readConfiguredHeaders(options.env ?? process.env);\n const catalog = buildRuleCatalog();\n const server = new McpServer(\n { name: \"goflag\", version },\n { instructions: instructionsFor(configured) },\n );\n\n server.registerTool(\n \"audit_site\",\n {\n title: \"Audit a site\",\n description:\n \"Crawl a site — a local dev server, a preview or production — and judge its SEO and i18n plumbing. Returns the findings rolled up by rule, each with its rigor, plus translation holes, broken links and the pages audited.\",\n inputSchema: auditShape,\n annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true },\n },\n async (args, extra) => {\n try {\n const report = await runAudit(args.url, auditOptions(args, extra, configured));\n return ok(headline(report), {\n exitCode: exitCode(report),\n coverage: report.diagnostics.coverage ?? null,\n summary: summarize(report),\n });\n } catch (err) {\n return failure((err as Error).message);\n }\n },\n );\n\n server.registerTool(\n \"compare_to_baseline\",\n {\n title: \"Compare to a baseline\",\n description:\n \"Audit a site and compare it to a report saved earlier with the goflag CLI (--report or --update-baseline). Returns what is new, what was resolved, and how many findings are unchanged: the question a change has to answer is whether it made things worse.\",\n inputSchema: {\n ...auditShape,\n baseline: z\n .string()\n .describe(\"Path to the saved JSON report, relative to where this server runs.\"),\n },\n annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true },\n },\n async ({ baseline: path, ...args }, extra) => {\n const baseline = await readBaseline(path);\n if (typeof baseline === \"string\") return failure(baseline);\n try {\n const report = await runAudit(args.url, auditOptions(args, extra, configured));\n const diff = diffReports(baseline, report);\n return ok(\n `${headline(report)} Against the baseline: ${diff.added.length} new, ${diff.resolved.length} resolved, ${diff.unchanged} unchanged.`,\n {\n exitCode: diffExitCode(diff, \"warning\", { total: totalFindings(report) }),\n baseline: diff.baseline,\n added: diff.added,\n resolved: diff.resolved,\n unchanged: diff.unchanged,\n ...(diff.profileMismatch ? { profileMismatch: diff.profileMismatch } : {}),\n ...(diff.indexingMismatch ? { indexingMismatch: diff.indexingMismatch } : {}),\n },\n );\n } catch (err) {\n return failure((err as Error).message);\n }\n },\n );\n\n server.registerTool(\n \"explain_rule\",\n {\n title: \"Explain a rule\",\n description:\n \"What one goflag rule checks, why, how authoritative it is (rigor), the documents that require it, and what a passing page looks like.\",\n inputSchema: {\n id: z.string().describe('A rule id from a finding, e.g. \"hreflang.cluster-incomplete\".'),\n },\n annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },\n },\n async ({ id }) => {\n const rule = catalog.rules.find((candidate) => candidate.id === id);\n if (!rule) {\n const family = id.split(\".\")[0] ?? \"\";\n const near = catalog.rules\n .filter((candidate) => candidate.id.startsWith(`${family}.`))\n .map((candidate) => candidate.id);\n return failure(\n `No rule \"${id}\".${near.length > 0 ? ` Rules in ${family}: ${near.join(\", \")}.` : \"\"} list_rules returns them all.`,\n );\n }\n return ok(`${rule.id}: ${rule.summary}`, ruleView(rule, catalog.sources));\n },\n );\n\n server.registerTool(\n \"list_rules\",\n {\n title: \"List the rules\",\n description:\n \"Every rule goflag runs, with its scope (page, site, prose), severity and rigor. Prose rules are questions for a reviewer, never findings.\",\n inputSchema: {\n scope: z\n .enum([\"page\", \"site\", \"prose\"])\n .optional()\n .describe(\"Only the rules of one scope.\"),\n },\n annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },\n },\n async ({ scope }) => {\n const rules = catalog.rules\n .filter((rule) => scope === undefined || rule.scope === scope)\n .map(({ id, scope: ruleScope, severity, rigor, summary }) => ({\n id,\n scope: ruleScope,\n severity,\n rigor,\n summary,\n }));\n return ok(`${rules.length} rule${rules.length === 1 ? \"\" : \"s\"}.`, { rules });\n },\n );\n\n return server;\n}\n","{\n \"name\": \"@goflag/mcp\",\n \"version\": \"0.1.2\",\n \"type\": \"module\",\n \"description\": \"A local MCP server for goflag: an agent audits the site it is editing — localhost or a protected preview — and reads each rule with its source.\",\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"seo\",\n \"i18n\",\n \"hreflang\",\n \"audit\",\n \"agent\",\n \"claude\",\n \"cursor\"\n ],\n \"license\": \"MIT\",\n \"author\": \"Tancrède Simonin <39413504+tancredesimonin@users.noreply.github.com>\",\n \"homepage\": \"https://github.com/tancredesimonin/goflag#readme\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/tancredesimonin/goflag.git\",\n \"directory\": \"packages/mcp\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/tancredesimonin/goflag/issues\"\n },\n \"mcpName\": \"io.github.tancredesimonin/goflag\",\n \"engines\": {\n \"node\": \">=22\"\n },\n \"bin\": {\n \"goflag-mcp\": \"dist/cli.js\"\n },\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"prepublishOnly\": \"pnpm build\",\n \"clean\": \"rm -rf dist coverage\",\n \"typecheck\": \"pnpm --filter @goflag/cli build && tsc --noEmit\",\n \"test\": \"pnpm --filter @goflag/cli build && vitest run\",\n \"test:unit\": \"pnpm --filter @goflag/cli build && vitest run\",\n \"test:watch\": \"vitest\",\n \"release\": \"commit-and-tag-version --path . --skip.tag --tag-prefix mcp-v\"\n },\n \"dependencies\": {\n \"@goflag/cli\": \"workspace:^\",\n \"@modelcontextprotocol/sdk\": \"^1.32.2\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@hono/node-server\": \"^2.0.1\",\n \"@types/node\": \"^22.10.5\",\n \"commit-and-tag-version\": \"^12.5.0\",\n \"hono\": \"^4.12.25\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^6.0.0\",\n \"vitest\": \"^5.0.5\"\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,43 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
14
14
|
*
|
|
15
15
|
* - No `start`. Letting an agent pass a command for this process to run is an
|
|
16
16
|
* arbitrary-execution door; the agent starts its own dev server.
|
|
17
|
-
* - No
|
|
18
|
-
* agent's transcript.
|
|
17
|
+
* - No header as a tool argument. A value passed there would sit in the
|
|
18
|
+
* agent's transcript. Headers come from the server's own configuration —
|
|
19
|
+
* `GOFLAG_HEADERS`, bound to the origins in `GOFLAG_HEADERS_ORIGIN` — and
|
|
20
|
+
* go to those origins only (`readConfiguredHeaders`).
|
|
19
21
|
* - No write. A baseline is captured with `goflag --update-baseline`; this
|
|
20
22
|
* server only reads one.
|
|
21
23
|
*/
|
|
22
24
|
|
|
25
|
+
/** What the server reads at start, beside the protocol. */
|
|
26
|
+
interface GoflagServerOptions {
|
|
27
|
+
/** Where `GOFLAG_HEADERS` and `GOFLAG_HEADERS_ORIGIN` are read. Defaults to `process.env`. */
|
|
28
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
}
|
|
30
|
+
/** Request headers from the server's configuration, and the origins they are for. */
|
|
31
|
+
interface ConfiguredHeaders {
|
|
32
|
+
origins: ReadonlySet<string>;
|
|
33
|
+
headers: Readonly<Record<string, string>>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The headers a protected preview needs, read from the server's environment.
|
|
37
|
+
*
|
|
38
|
+
* GOFLAG_HEADERS one `Name: value` per line, as `--header` takes it
|
|
39
|
+
* GOFLAG_HEADERS_ORIGIN the origins they are for, comma-separated
|
|
40
|
+
*
|
|
41
|
+
* Bound to origins because here, unlike on a command line, the URL is chosen
|
|
42
|
+
* by the agent, call by call. Sent wherever it pointed, a preview's token would
|
|
43
|
+
* go to any site a prompt injection named; bound, it goes to the origins the
|
|
44
|
+
* person configured and nowhere else, which is the CLI's own rule — the header
|
|
45
|
+
* goes to the origin the operator named — kept when the operator is not the one
|
|
46
|
+
* typing the URL.
|
|
47
|
+
*
|
|
48
|
+
* A configuration that cannot be honoured stops the server rather than
|
|
49
|
+
* running without it: headers with no origin, an origin that is not one, a line
|
|
50
|
+
* `parseHeaderArg` refuses. Errors name the header and never quote its value.
|
|
51
|
+
*/
|
|
52
|
+
declare function readConfiguredHeaders(env: Readonly<Record<string, string | undefined>>): ConfiguredHeaders | undefined;
|
|
23
53
|
/** A server with goflag's four tools, not yet connected to a transport. */
|
|
24
|
-
declare function createGoflagServer(): McpServer;
|
|
54
|
+
declare function createGoflagServer(options?: GoflagServerOptions): McpServer;
|
|
25
55
|
|
|
26
|
-
export { createGoflagServer };
|
|
56
|
+
export { type ConfiguredHeaders, type GoflagServerOptions, createGoflagServer, readConfiguredHeaders };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
createGoflagServer
|
|
4
|
-
|
|
3
|
+
createGoflagServer,
|
|
4
|
+
readConfiguredHeaders
|
|
5
|
+
} from "./chunk-NW2SIYA2.js";
|
|
5
6
|
export {
|
|
6
|
-
createGoflagServer
|
|
7
|
+
createGoflagServer,
|
|
8
|
+
readConfiguredHeaders
|
|
7
9
|
};
|
|
8
10
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goflag/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A local MCP server for goflag: an agent audits the site it is editing — localhost or a protected preview — and reads each rule with its source.",
|
|
6
6
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@goflag/cli": "^0.2.
|
|
50
|
+
"@goflag/cli": "^0.2.20",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
52
52
|
"zod": "^4.3.6"
|
|
53
53
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../package.json"],"sourcesContent":["/**\n * The goflag MCP server: four read-only tools over the CLI's own engine.\n *\n * Local on purpose (`docs/integrations-plan.md` D1). An MCP server that runs\n * somewhere else cannot see `localhost:3000`, nor a preview behind a company\n * VPN; this one runs where the agent runs and sees what it sees. It is also\n * thin on purpose: `runAudit`, `summarize`, `diffReports` and the rule\n * catalogue are the CLI's, imported by name, so an answer here is the answer\n * `goflag` would print — never a second opinion that drifts from it.\n *\n * What it deliberately does not offer:\n *\n * - No `start`. Letting an agent pass a command for this process to run is an\n * arbitrary-execution door; the agent starts its own dev server.\n * - No headers. A `--header` value passed as a tool argument would sit in the\n * agent's transcript. A protected preview is audited with the CLI.\n * - No write. A baseline is captured with `goflag --update-baseline`; this\n * server only reads one.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { extname } from \"node:path\";\n\nimport {\n buildRuleCatalog,\n diffExitCode,\n diffReports,\n exitCode,\n runAudit,\n summarize,\n totalFindings,\n type AuditOptions,\n type CatalogRule,\n type GoflagReport,\n type ProgressEvent,\n} from \"@goflag/cli\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { RequestHandlerExtra } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport type {\n CallToolResult,\n ServerNotification,\n ServerRequest,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { version } from \"../package.json\";\n\n/** Where a rule explains itself, for a human the agent hands a link to. */\nconst RULE_DOCS = \"https://goflag.tech/docs/rules/\";\n\n/**\n * The page budget when the agent sets none. The CLI's is 200, sized for a CI\n * job that has minutes; an agent waits on the answer, and on openfinanceguide\n * the link check alone took 618 s of a 715 s run (0.2.15). An agent can ask for\n * more; a first call should come back fast.\n */\nconst DEFAULT_MAX_PAGES = 50;\n\n/** One progress notification per this many milliseconds at most, phase changes aside. */\nconst PROGRESS_INTERVAL_MS = 500;\n\nconst INSTRUCTIONS = `goflag audits a website's SEO and i18n plumbing: hreflang clusters, translation holes, sitemap and robots.txt agreement, canonical, Open Graph, icons, llms.txt, soft 404s.\n\nUse it on the site you are editing, before committing a change to metadata, the sitemap, robots.txt or hreflang: start the dev server, call audit_site on its URL, fix what is new, and call it again. explain_rule gives a rule's requirement, how authoritative it is, and the documents behind it. compare_to_baseline answers \"did my change make it worse?\" against a report saved with the goflag CLI.\n\nFindings carry a rigor: spec-required and vendor-spec are requirements, guideline and heuristic are advice.`;\n\nconst auditShape = {\n url: z\n .string()\n .describe(\"The site to audit, http or https. A dev server's URL works: http://localhost:3000.\"),\n maxPages: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(`Page budget. Default ${DEFAULT_MAX_PAGES}.`),\n locales: z\n .array(z.string())\n .optional()\n .describe('Locales the site serves, e.g. [\"en\", \"fr\"], when its sitemap does not say.'),\n expectIndexing: z\n .enum([\"no\", \"yes\"])\n .optional()\n .describe('\"no\" for a staging or preview host that must stay out of search indexes.'),\n static: z\n .boolean()\n .optional()\n .describe(\"Never launch Chromium; judge the server-rendered HTML only.\"),\n advisories: z\n .boolean()\n .optional()\n .describe(\n \"Also return the questions a linter cannot settle, with their evidence, for you to judge.\",\n ),\n checkExternal: z\n .boolean()\n .optional()\n .describe(\"Also probe links to other sites. Off by default: slow, and not your site's fault.\"),\n};\n\ntype AuditArgs = {\n url: string;\n maxPages?: number;\n locales?: string[];\n expectIndexing?: \"no\" | \"yes\";\n static?: boolean;\n advisories?: boolean;\n checkExternal?: boolean;\n};\n\ntype ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;\n\nfunction auditOptions(args: AuditArgs, extra: ToolExtra): AuditOptions {\n return {\n maxPages: args.maxPages ?? DEFAULT_MAX_PAGES,\n locales: args.locales,\n expectIndexing: args.expectIndexing,\n static: args.static,\n advisories: args.advisories,\n checkExternal: args.checkExternal ?? false,\n signal: extra.signal,\n onProgress: progressReporter(extra),\n };\n}\n\n/**\n * Crawl progress as MCP progress notifications, when the client asked for them\n * (a `progressToken`). Throttled: a crawl reports every page, and a client\n * drawing a spinner needs a heartbeat, not a stream. `progress` only grows, as\n * the protocol requires; the crawl's own total grows as links are found, so it\n * goes in the message rather than in `total`.\n */\nfunction progressReporter(extra: ToolExtra): ((event: ProgressEvent) => void) | undefined {\n const token = extra._meta?.progressToken;\n if (token === undefined) return undefined;\n let progress = 0;\n let lastPhase = \"\";\n let lastSent = 0;\n return (event) => {\n progress += 1;\n const now = Date.now();\n if (event.phase === lastPhase && now - lastSent < PROGRESS_INTERVAL_MS) return;\n lastPhase = event.phase;\n lastSent = now;\n extra\n .sendNotification({\n method: \"notifications/progress\",\n params: {\n progressToken: token,\n progress,\n message: `${event.phase}: ${event.done} of ${event.total}`,\n },\n })\n .catch(() => undefined);\n };\n}\n\nfunction ok(text: string, structured: Record<string, unknown>): CallToolResult {\n return {\n content: [{ type: \"text\", text: `${text}\\n\\n${JSON.stringify(structured)}` }],\n structuredContent: structured,\n };\n}\n\nfunction failure(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/** One line a human can read before the JSON an agent reads. */\nfunction headline(report: GoflagReport): string {\n const total = totalFindings(report);\n const coverage = report.diagnostics.coverage;\n const pages =\n coverage?.selected !== undefined && coverage.considered !== undefined\n ? `${coverage.selected} of ${coverage.considered} sitemap URLs`\n : `${report.diagnostics.pagesCrawled} pages`;\n return `${report.url}: ${report.summary.verdict} flag, ${total} finding${total === 1 ? \"\" : \"s\"}, ${pages} audited.`;\n}\n\n/**\n * Read a report saved by the CLI. Errors say what was wrong with the file and\n * never quote it: an agent can point this at any path, and a parse error that\n * echoes the first bytes of `~/.ssh/id_ed25519` into the transcript would make\n * a read-only tool a way to read files the agent was not shown.\n */\nasync function readBaseline(path: string): Promise<GoflagReport | string> {\n if (extname(path).toLowerCase() !== \".json\") {\n return `${path} is not a .json file. Pass a report saved by goflag --report or --update-baseline.`;\n }\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n return `Could not read ${path}.`;\n }\n try {\n const parsed = JSON.parse(raw) as Partial<GoflagReport>;\n if (typeof parsed.url === \"string\" && parsed.summary && Array.isArray(parsed.seoIssues)) {\n return parsed as GoflagReport;\n }\n } catch {\n // Falls through to the same answer as a JSON file of the wrong shape.\n }\n return `${path} is not a goflag JSON report.`;\n}\n\nfunction ruleView(rule: CatalogRule, sources: ReturnType<typeof buildRuleCatalog>[\"sources\"]) {\n return {\n ...rule,\n sources: rule.sources.map((id) => sources[id] ?? { id }),\n docs: `${RULE_DOCS}${rule.id}`,\n };\n}\n\n/** A server with goflag's four tools, not yet connected to a transport. */\nexport function createGoflagServer(): McpServer {\n const catalog = buildRuleCatalog();\n const server = new McpServer({ name: \"goflag\", version }, { instructions: INSTRUCTIONS });\n\n server.registerTool(\n \"audit_site\",\n {\n title: \"Audit a site\",\n description:\n \"Crawl a site — a local dev server, a preview or production — and judge its SEO and i18n plumbing. Returns the findings rolled up by rule, each with its rigor, plus translation holes, broken links and the pages audited.\",\n inputSchema: auditShape,\n annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true },\n },\n async (args, extra) => {\n try {\n const report = await runAudit(args.url, auditOptions(args, extra));\n return ok(headline(report), {\n exitCode: exitCode(report),\n coverage: report.diagnostics.coverage ?? null,\n summary: summarize(report),\n });\n } catch (err) {\n return failure((err as Error).message);\n }\n },\n );\n\n server.registerTool(\n \"compare_to_baseline\",\n {\n title: \"Compare to a baseline\",\n description:\n \"Audit a site and compare it to a report saved earlier with the goflag CLI (--report or --update-baseline). Returns what is new, what was resolved, and how many findings are unchanged: the question a change has to answer is whether it made things worse.\",\n inputSchema: {\n ...auditShape,\n baseline: z\n .string()\n .describe(\"Path to the saved JSON report, relative to where this server runs.\"),\n },\n annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true },\n },\n async ({ baseline: path, ...args }, extra) => {\n const baseline = await readBaseline(path);\n if (typeof baseline === \"string\") return failure(baseline);\n try {\n const report = await runAudit(args.url, auditOptions(args, extra));\n const diff = diffReports(baseline, report);\n return ok(\n `${headline(report)} Against the baseline: ${diff.added.length} new, ${diff.resolved.length} resolved, ${diff.unchanged} unchanged.`,\n {\n exitCode: diffExitCode(diff, \"warning\", { total: totalFindings(report) }),\n baseline: diff.baseline,\n added: diff.added,\n resolved: diff.resolved,\n unchanged: diff.unchanged,\n ...(diff.profileMismatch ? { profileMismatch: diff.profileMismatch } : {}),\n ...(diff.indexingMismatch ? { indexingMismatch: diff.indexingMismatch } : {}),\n },\n );\n } catch (err) {\n return failure((err as Error).message);\n }\n },\n );\n\n server.registerTool(\n \"explain_rule\",\n {\n title: \"Explain a rule\",\n description:\n \"What one goflag rule checks, why, how authoritative it is (rigor), the documents that require it, and what a passing page looks like.\",\n inputSchema: {\n id: z.string().describe('A rule id from a finding, e.g. \"hreflang.cluster-incomplete\".'),\n },\n annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },\n },\n async ({ id }) => {\n const rule = catalog.rules.find((candidate) => candidate.id === id);\n if (!rule) {\n const family = id.split(\".\")[0] ?? \"\";\n const near = catalog.rules\n .filter((candidate) => candidate.id.startsWith(`${family}.`))\n .map((candidate) => candidate.id);\n return failure(\n `No rule \"${id}\".${near.length > 0 ? ` Rules in ${family}: ${near.join(\", \")}.` : \"\"} list_rules returns them all.`,\n );\n }\n return ok(`${rule.id}: ${rule.summary}`, ruleView(rule, catalog.sources));\n },\n );\n\n server.registerTool(\n \"list_rules\",\n {\n title: \"List the rules\",\n description:\n \"Every rule goflag runs, with its scope (page, site, prose), severity and rigor. Prose rules are questions for a reviewer, never findings.\",\n inputSchema: {\n scope: z\n .enum([\"page\", \"site\", \"prose\"])\n .optional()\n .describe(\"Only the rules of one scope.\"),\n },\n annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },\n },\n async ({ scope }) => {\n const rules = catalog.rules\n .filter((rule) => scope === undefined || rule.scope === scope)\n .map(({ id, scope: ruleScope, severity, rigor, summary }) => ({\n id,\n scope: ruleScope,\n severity,\n rigor,\n summary,\n }));\n return ok(`${rules.length} rule${rules.length === 1 ? \"\" : \"s\"}.`, { rules });\n },\n );\n\n return server;\n}\n","{\n \"name\": \"@goflag/mcp\",\n \"version\": \"0.1.0\",\n \"type\": \"module\",\n \"description\": \"A local MCP server for goflag: an agent audits the site it is editing — localhost or a protected preview — and reads each rule with its source.\",\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"seo\",\n \"i18n\",\n \"hreflang\",\n \"audit\",\n \"agent\",\n \"claude\",\n \"cursor\"\n ],\n \"license\": \"MIT\",\n \"author\": \"Tancrède Simonin <39413504+tancredesimonin@users.noreply.github.com>\",\n \"homepage\": \"https://github.com/tancredesimonin/goflag#readme\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/tancredesimonin/goflag.git\",\n \"directory\": \"packages/mcp\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/tancredesimonin/goflag/issues\"\n },\n \"mcpName\": \"io.github.tancredesimonin/goflag\",\n \"engines\": {\n \"node\": \">=22\"\n },\n \"bin\": {\n \"goflag-mcp\": \"dist/cli.js\"\n },\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"prepublishOnly\": \"pnpm build\",\n \"clean\": \"rm -rf dist coverage\",\n \"typecheck\": \"pnpm --filter @goflag/cli build && tsc --noEmit\",\n \"test\": \"pnpm --filter @goflag/cli build && vitest run\",\n \"test:unit\": \"pnpm --filter @goflag/cli build && vitest run\",\n \"test:watch\": \"vitest\",\n \"release\": \"commit-and-tag-version --path . --skip.tag --tag-prefix mcp-v\"\n },\n \"dependencies\": {\n \"@goflag/cli\": \"workspace:^\",\n \"@modelcontextprotocol/sdk\": \"^1.32.2\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@hono/node-server\": \"^2.0.1\",\n \"@types/node\": \"^22.10.5\",\n \"commit-and-tag-version\": \"^12.5.0\",\n \"hono\": \"^4.12.25\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^6.0.0\",\n \"vitest\": \"^5.0.5\"\n }\n}\n"],"mappings":"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","names":[]}
|