@goflag/mcp 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/dist/chunk-5KYL7Z7T.js +225 -0
- package/dist/chunk-5KYL7Z7T.js.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tancrède Simonin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @goflag/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
goflag as an MCP server. An agent audits the site it is editing — a dev server on `localhost`, a preview behind a VPN — and reads each rule with the documents that require it.
|
|
4
|
+
|
|
5
|
+
It runs locally, over stdio, on the same engine as [`@goflag/cli`](https://www.npmjs.com/package/@goflag/cli): an answer here is the answer `goflag` would print.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
Claude Code:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
claude mcp add goflag -- npx -y @goflag/mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Cursor (`.cursor/mcp.json`), VS Code (`.vscode/mcp.json`, under `servers`) and other clients take the same command:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"goflag": { "command": "npx", "args": ["-y", "@goflag/mcp"] }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then ask your agent to check the SEO of what it just changed: it starts the dev server, calls `audit_site` on its URL, fixes what is new, and calls it again.
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
All four are read-only: none writes to disk or to the site.
|
|
30
|
+
|
|
31
|
+
| Tool | Input | Returns |
|
|
32
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `audit_site` | `url`; optional `maxPages` (default 50), `locales`, `expectIndexing`, `static`, `advisories`, `checkExternal` (default off) | The findings rolled up by rule with their rigor, translation holes, broken links, the coverage, and the exit code the CLI would return |
|
|
34
|
+
| `compare_to_baseline` | the same, plus `baseline`: a JSON report saved by `goflag --report` or `--update-baseline` | What is new, what was resolved, how many findings are unchanged |
|
|
35
|
+
| `explain_rule` | `id`, from a finding | What the rule checks, why, its rigor, the documents behind it, and its page on goflag.tech |
|
|
36
|
+
| `list_rules` | optional `scope`: `page`, `site` or `prose` | Every rule with its scope, severity and rigor |
|
|
37
|
+
|
|
38
|
+
`audit_site` reports crawl progress to a client that asks for it, and stops when the call is cancelled.
|
|
39
|
+
|
|
40
|
+
## What it does not do
|
|
41
|
+
|
|
42
|
+
- **No headers.** A token passed as a tool argument would sit in the agent's transcript. Audit a protected preview with the CLI: `goflag <url> --header "<name>: <value>"`.
|
|
43
|
+
- **No command to run.** The agent starts its own dev server; this server never executes one.
|
|
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
|
+
- **No remote mode.** It runs where the agent runs — which is why it can see `localhost`.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
## Learn more
|
|
52
|
+
|
|
53
|
+
- [The rules](https://goflag.tech/docs/rules) — each with its rigor and its sources
|
|
54
|
+
- [The CLI](https://www.npmjs.com/package/@goflag/cli) — the gate in CI, the HTML report, `--header`
|
|
55
|
+
- [Source and issues](https://github.com/tancredesimonin/goflag)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { extname } from "path";
|
|
6
|
+
import {
|
|
7
|
+
buildRuleCatalog,
|
|
8
|
+
diffExitCode,
|
|
9
|
+
diffReports,
|
|
10
|
+
exitCode,
|
|
11
|
+
runAudit,
|
|
12
|
+
summarize,
|
|
13
|
+
totalFindings
|
|
14
|
+
} from "@goflag/cli";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
// package.json
|
|
19
|
+
var version = "0.1.0";
|
|
20
|
+
|
|
21
|
+
// src/server.ts
|
|
22
|
+
var RULE_DOCS = "https://goflag.tech/docs/rules/";
|
|
23
|
+
var DEFAULT_MAX_PAGES = 50;
|
|
24
|
+
var PROGRESS_INTERVAL_MS = 500;
|
|
25
|
+
var 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.
|
|
26
|
+
|
|
27
|
+
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
|
+
Findings carry a rigor: spec-required and vendor-spec are requirements, guideline and heuristic are advice.`;
|
|
30
|
+
var auditShape = {
|
|
31
|
+
url: z.string().describe("The site to audit, http or https. A dev server's URL works: http://localhost:3000."),
|
|
32
|
+
maxPages: z.number().int().min(1).max(500).optional().describe(`Page budget. Default ${DEFAULT_MAX_PAGES}.`),
|
|
33
|
+
locales: z.array(z.string()).optional().describe('Locales the site serves, e.g. ["en", "fr"], when its sitemap does not say.'),
|
|
34
|
+
expectIndexing: z.enum(["no", "yes"]).optional().describe('"no" for a staging or preview host that must stay out of search indexes.'),
|
|
35
|
+
static: z.boolean().optional().describe("Never launch Chromium; judge the server-rendered HTML only."),
|
|
36
|
+
advisories: z.boolean().optional().describe(
|
|
37
|
+
"Also return the questions a linter cannot settle, with their evidence, for you to judge."
|
|
38
|
+
),
|
|
39
|
+
checkExternal: z.boolean().optional().describe("Also probe links to other sites. Off by default: slow, and not your site's fault.")
|
|
40
|
+
};
|
|
41
|
+
function auditOptions(args, extra) {
|
|
42
|
+
return {
|
|
43
|
+
maxPages: args.maxPages ?? DEFAULT_MAX_PAGES,
|
|
44
|
+
locales: args.locales,
|
|
45
|
+
expectIndexing: args.expectIndexing,
|
|
46
|
+
static: args.static,
|
|
47
|
+
advisories: args.advisories,
|
|
48
|
+
checkExternal: args.checkExternal ?? false,
|
|
49
|
+
signal: extra.signal,
|
|
50
|
+
onProgress: progressReporter(extra)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function progressReporter(extra) {
|
|
54
|
+
const token = extra._meta?.progressToken;
|
|
55
|
+
if (token === void 0) return void 0;
|
|
56
|
+
let progress = 0;
|
|
57
|
+
let lastPhase = "";
|
|
58
|
+
let lastSent = 0;
|
|
59
|
+
return (event) => {
|
|
60
|
+
progress += 1;
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (event.phase === lastPhase && now - lastSent < PROGRESS_INTERVAL_MS) return;
|
|
63
|
+
lastPhase = event.phase;
|
|
64
|
+
lastSent = now;
|
|
65
|
+
extra.sendNotification({
|
|
66
|
+
method: "notifications/progress",
|
|
67
|
+
params: {
|
|
68
|
+
progressToken: token,
|
|
69
|
+
progress,
|
|
70
|
+
message: `${event.phase}: ${event.done} of ${event.total}`
|
|
71
|
+
}
|
|
72
|
+
}).catch(() => void 0);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function ok(text, structured) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: `${text}
|
|
78
|
+
|
|
79
|
+
${JSON.stringify(structured)}` }],
|
|
80
|
+
structuredContent: structured
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function failure(text) {
|
|
84
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
85
|
+
}
|
|
86
|
+
function headline(report) {
|
|
87
|
+
const total = totalFindings(report);
|
|
88
|
+
const coverage = report.diagnostics.coverage;
|
|
89
|
+
const pages = coverage?.selected !== void 0 && coverage.considered !== void 0 ? `${coverage.selected} of ${coverage.considered} sitemap URLs` : `${report.diagnostics.pagesCrawled} pages`;
|
|
90
|
+
return `${report.url}: ${report.summary.verdict} flag, ${total} finding${total === 1 ? "" : "s"}, ${pages} audited.`;
|
|
91
|
+
}
|
|
92
|
+
async function readBaseline(path) {
|
|
93
|
+
if (extname(path).toLowerCase() !== ".json") {
|
|
94
|
+
return `${path} is not a .json file. Pass a report saved by goflag --report or --update-baseline.`;
|
|
95
|
+
}
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = await readFile(path, "utf8");
|
|
99
|
+
} catch {
|
|
100
|
+
return `Could not read ${path}.`;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
if (typeof parsed.url === "string" && parsed.summary && Array.isArray(parsed.seoIssues)) {
|
|
105
|
+
return parsed;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return `${path} is not a goflag JSON report.`;
|
|
110
|
+
}
|
|
111
|
+
function ruleView(rule, sources) {
|
|
112
|
+
return {
|
|
113
|
+
...rule,
|
|
114
|
+
sources: rule.sources.map((id) => sources[id] ?? { id }),
|
|
115
|
+
docs: `${RULE_DOCS}${rule.id}`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function createGoflagServer() {
|
|
119
|
+
const catalog = buildRuleCatalog();
|
|
120
|
+
const server = new McpServer({ name: "goflag", version }, { instructions: INSTRUCTIONS });
|
|
121
|
+
server.registerTool(
|
|
122
|
+
"audit_site",
|
|
123
|
+
{
|
|
124
|
+
title: "Audit a site",
|
|
125
|
+
description: "Crawl a site \u2014 a local dev server, a preview or production \u2014 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.",
|
|
126
|
+
inputSchema: auditShape,
|
|
127
|
+
annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true }
|
|
128
|
+
},
|
|
129
|
+
async (args, extra) => {
|
|
130
|
+
try {
|
|
131
|
+
const report = await runAudit(args.url, auditOptions(args, extra));
|
|
132
|
+
return ok(headline(report), {
|
|
133
|
+
exitCode: exitCode(report),
|
|
134
|
+
coverage: report.diagnostics.coverage ?? null,
|
|
135
|
+
summary: summarize(report)
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return failure(err.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
server.registerTool(
|
|
143
|
+
"compare_to_baseline",
|
|
144
|
+
{
|
|
145
|
+
title: "Compare to a baseline",
|
|
146
|
+
description: "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.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
...auditShape,
|
|
149
|
+
baseline: z.string().describe("Path to the saved JSON report, relative to where this server runs.")
|
|
150
|
+
},
|
|
151
|
+
annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true }
|
|
152
|
+
},
|
|
153
|
+
async ({ baseline: path, ...args }, extra) => {
|
|
154
|
+
const baseline = await readBaseline(path);
|
|
155
|
+
if (typeof baseline === "string") return failure(baseline);
|
|
156
|
+
try {
|
|
157
|
+
const report = await runAudit(args.url, auditOptions(args, extra));
|
|
158
|
+
const diff = diffReports(baseline, report);
|
|
159
|
+
return ok(
|
|
160
|
+
`${headline(report)} Against the baseline: ${diff.added.length} new, ${diff.resolved.length} resolved, ${diff.unchanged} unchanged.`,
|
|
161
|
+
{
|
|
162
|
+
exitCode: diffExitCode(diff, "warning", { total: totalFindings(report) }),
|
|
163
|
+
baseline: diff.baseline,
|
|
164
|
+
added: diff.added,
|
|
165
|
+
resolved: diff.resolved,
|
|
166
|
+
unchanged: diff.unchanged,
|
|
167
|
+
...diff.profileMismatch ? { profileMismatch: diff.profileMismatch } : {},
|
|
168
|
+
...diff.indexingMismatch ? { indexingMismatch: diff.indexingMismatch } : {}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return failure(err.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
server.registerTool(
|
|
177
|
+
"explain_rule",
|
|
178
|
+
{
|
|
179
|
+
title: "Explain a rule",
|
|
180
|
+
description: "What one goflag rule checks, why, how authoritative it is (rigor), the documents that require it, and what a passing page looks like.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
id: z.string().describe('A rule id from a finding, e.g. "hreflang.cluster-incomplete".')
|
|
183
|
+
},
|
|
184
|
+
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true }
|
|
185
|
+
},
|
|
186
|
+
async ({ id }) => {
|
|
187
|
+
const rule = catalog.rules.find((candidate) => candidate.id === id);
|
|
188
|
+
if (!rule) {
|
|
189
|
+
const family = id.split(".")[0] ?? "";
|
|
190
|
+
const near = catalog.rules.filter((candidate) => candidate.id.startsWith(`${family}.`)).map((candidate) => candidate.id);
|
|
191
|
+
return failure(
|
|
192
|
+
`No rule "${id}".${near.length > 0 ? ` Rules in ${family}: ${near.join(", ")}.` : ""} list_rules returns them all.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return ok(`${rule.id}: ${rule.summary}`, ruleView(rule, catalog.sources));
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
server.registerTool(
|
|
199
|
+
"list_rules",
|
|
200
|
+
{
|
|
201
|
+
title: "List the rules",
|
|
202
|
+
description: "Every rule goflag runs, with its scope (page, site, prose), severity and rigor. Prose rules are questions for a reviewer, never findings.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
scope: z.enum(["page", "site", "prose"]).optional().describe("Only the rules of one scope.")
|
|
205
|
+
},
|
|
206
|
+
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true }
|
|
207
|
+
},
|
|
208
|
+
async ({ scope }) => {
|
|
209
|
+
const rules = catalog.rules.filter((rule) => scope === void 0 || rule.scope === scope).map(({ id, scope: ruleScope, severity, rigor, summary }) => ({
|
|
210
|
+
id,
|
|
211
|
+
scope: ruleScope,
|
|
212
|
+
severity,
|
|
213
|
+
rigor,
|
|
214
|
+
summary
|
|
215
|
+
}));
|
|
216
|
+
return ok(`${rules.length} rule${rules.length === 1 ? "" : "s"}.`, { rules });
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
return server;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
createGoflagServer
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=chunk-5KYL7Z7T.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 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":[]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createGoflagServer
|
|
4
|
+
} from "./chunk-5KYL7Z7T.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
try {
|
|
9
|
+
await createGoflagServer().connect(new StdioServerTransport());
|
|
10
|
+
} catch (err) {
|
|
11
|
+
process.stderr.write(`goflag-mcp: ${err.message}
|
|
12
|
+
`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * `goflag-mcp`: the server over stdio, which is how an MCP client spawns it.\n *\n * stdout carries the protocol and nothing else — the audit engine writes\n * nowhere, and nothing here may either. A diagnostic goes to stderr.\n */\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { createGoflagServer } from \"./server\";\n\ntry {\n await createGoflagServer().connect(new StdioServerTransport());\n} catch (err) {\n process.stderr.write(`goflag-mcp: ${(err as Error).message}\\n`);\n process.exit(1);\n}\n"],"mappings":";;;;;;AAMA,SAAS,4BAA4B;AAIrC,IAAI;AACF,QAAM,mBAAmB,EAAE,QAAQ,IAAI,qBAAqB,CAAC;AAC/D,SAAS,KAAK;AACZ,UAAQ,OAAO,MAAM,eAAgB,IAAc,OAAO;AAAA,CAAI;AAC9D,UAAQ,KAAK,CAAC;AAChB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The goflag MCP server: four read-only tools over the CLI's own engine.
|
|
5
|
+
*
|
|
6
|
+
* Local on purpose (`docs/integrations-plan.md` D1). An MCP server that runs
|
|
7
|
+
* somewhere else cannot see `localhost:3000`, nor a preview behind a company
|
|
8
|
+
* VPN; this one runs where the agent runs and sees what it sees. It is also
|
|
9
|
+
* thin on purpose: `runAudit`, `summarize`, `diffReports` and the rule
|
|
10
|
+
* catalogue are the CLI's, imported by name, so an answer here is the answer
|
|
11
|
+
* `goflag` would print — never a second opinion that drifts from it.
|
|
12
|
+
*
|
|
13
|
+
* What it deliberately does not offer:
|
|
14
|
+
*
|
|
15
|
+
* - No `start`. Letting an agent pass a command for this process to run is an
|
|
16
|
+
* arbitrary-execution door; the agent starts its own dev server.
|
|
17
|
+
* - No headers. A `--header` value passed as a tool argument would sit in the
|
|
18
|
+
* agent's transcript. A protected preview is audited with the CLI.
|
|
19
|
+
* - No write. A baseline is captured with `goflag --update-baseline`; this
|
|
20
|
+
* server only reads one.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** A server with goflag's four tools, not yet connected to a transport. */
|
|
24
|
+
declare function createGoflagServer(): McpServer;
|
|
25
|
+
|
|
26
|
+
export { createGoflagServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goflag/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"seo",
|
|
10
|
+
"i18n",
|
|
11
|
+
"hreflang",
|
|
12
|
+
"audit",
|
|
13
|
+
"agent",
|
|
14
|
+
"claude",
|
|
15
|
+
"cursor"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Tancrède Simonin <39413504+tancredesimonin@users.noreply.github.com>",
|
|
19
|
+
"homepage": "https://github.com/tancredesimonin/goflag#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/tancredesimonin/goflag.git",
|
|
23
|
+
"directory": "packages/mcp"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/tancredesimonin/goflag/issues"
|
|
27
|
+
},
|
|
28
|
+
"mcpName": "io.github.tancredesimonin/goflag",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"goflag-mcp": "dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@goflag/cli": "^0.2.18",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
52
|
+
"zod": "^4.3.6"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@hono/node-server": "^2.0.1",
|
|
56
|
+
"@types/node": "^22.10.5",
|
|
57
|
+
"commit-and-tag-version": "^12.5.0",
|
|
58
|
+
"hono": "^4.12.25",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"typescript": "^6.0.0",
|
|
61
|
+
"vitest": "^5.0.5"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup",
|
|
65
|
+
"clean": "rm -rf dist coverage",
|
|
66
|
+
"typecheck": "pnpm --filter @goflag/cli build && tsc --noEmit",
|
|
67
|
+
"test": "pnpm --filter @goflag/cli build && vitest run",
|
|
68
|
+
"test:unit": "pnpm --filter @goflag/cli build && vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"release": "commit-and-tag-version --path . --skip.tag --tag-prefix mcp-v"
|
|
71
|
+
}
|
|
6
72
|
}
|