mcp-app-lint 0.1.7 → 0.1.8
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 +11 -3
- package/dist/analyze.d.mts +1 -0
- package/dist/analyze.d.mts.map +1 -1
- package/dist/cli.d.mts.map +1 -1
- package/dist/csp.d.mts +11 -0
- package/dist/csp.d.mts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/server.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/analyze.mjs +4 -1
- package/src/cli.mjs +29 -10
- package/src/csp.mjs +40 -2
- package/src/index.mjs +1 -1
- package/src/server.mjs +8 -3
package/README.md
CHANGED
|
@@ -56,12 +56,20 @@ keyed by document (or one block per document with `--format text`):
|
|
|
56
56
|
|
|
57
57
|
The keys follow the host formula in the specification: `connectDomains` for `fetch`, `WebSocket`,
|
|
58
58
|
`EventSource`, XHR and `sendBeacon`; `resourceDomains` for scripts (including module imports from a
|
|
59
|
-
URL), stylesheets and CSS `url()`/`@import
|
|
59
|
+
URL), stylesheets and CSS `url()`/`@import` (in `<style>` and `style` attributes), images (`src`,
|
|
60
|
+
`srcset`, `poster`) and media;
|
|
60
61
|
`frameDomains` for iframes. Links (`<a href>`, `rel="canonical"`, `preconnect`) need nothing. A
|
|
61
62
|
Google Fonts stylesheet brings `fonts.gstatic.com` with it. It is static: a URL the script assembles
|
|
62
63
|
at run time is not seen, so run it on the built document and add such hosts by hand. `null` means
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
no origin has a known key. MCPAPP001 carries the same suggestion in its message and in
|
|
65
|
+
`properties.suggestedCsp`.
|
|
66
|
+
|
|
67
|
+
A bundle often holds a URL as a plain string that a library fetches later: a map tile template
|
|
68
|
+
(`url: "https://tile.example.org/{z}/{x}/{y}.png"`), a CDN base URL in a template literal. The key
|
|
69
|
+
cannot be told from the code, so these origins are listed as hints, not put into the declaration:
|
|
70
|
+
on a line of their own with `--format text`, on stderr next to the JSON (stdout stays the
|
|
71
|
+
declaration to copy), and as `mentioned` from the MCP tool. Link targets, XML namespaces,
|
|
72
|
+
framework documentation hosts and placeholders such as `*.example.com` are left out.
|
|
65
73
|
|
|
66
74
|
## Use in CI (GitHub code scanning)
|
|
67
75
|
|
package/dist/analyze.d.mts
CHANGED
|
@@ -138,6 +138,7 @@ export declare function analyzeHtml(html: unknown): {
|
|
|
138
138
|
networkHosts: string[];
|
|
139
139
|
sinkHosts: string[];
|
|
140
140
|
cspNeeds: import("./csp.mjs").CspNeeds;
|
|
141
|
+
cspMentioned: string[];
|
|
141
142
|
};
|
|
142
143
|
/**
|
|
143
144
|
* Short lines and not too many of them: written by a person, not emitted by a bundler.
|
package/dist/analyze.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAiBG,YAA+B,MAAM,GAA3B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAQ;AAEpC,YAAoF,MAAM,GAAhF,gBAAgB,GAAG,iBAAiB,GAAG,cAAc,GAAG,gBAAgB,CAAQ;AAK9F,eAAO,MAAM,OAAO,8BAA8B,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,MAAM,GAAG,IAAI,CAWzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,OAEuB;;;;;;;;;;;;;;;;;;;;;;EAoBjC;AAeD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAH7B,OAG6B,EAAE,QAAQ,EAFvC,OAEuC;;;;;;;;;;;;;;EAWjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAFvB,OAEuB;;;;;;EAkBjC;AAED,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,UAA0G,CAAC;AAyDrI,YAA+D,eAAe,GAApE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAiB;AAI9E,YAAkB,QAAQ,GAC1B;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAoB,WAAW,EAApB,OAAO,CAClB;;;;IAAoB,OAAO,EAAhB,OAAO,CAClB;IAAmB,OAAO,EAAf,MAAM,CACnB;CAAA,CAAA;AAZD;;;GAGG;AACH;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAH9B,MAG8B,GAF5B,eAAe,EAAE,CAc7B;AAqBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAFrB,OAEqB;;;;qBAMA,MAAM,EAAE;;;;;;;kBAOX,KAAK,CAAC,eAAe,GAAG;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;;;;WAIxD,QAAQ,EAAE;;;;;;mBAMF,MAAM,EAAE;kBAET,MAAM,EAAE;eACX,MAAM,EAAE;;
|
|
1
|
+
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAiBG,YAA+B,MAAM,GAA3B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAQ;AAEpC,YAAoF,MAAM,GAAhF,gBAAgB,GAAG,iBAAiB,GAAG,cAAc,GAAG,gBAAgB,CAAQ;AAK9F,eAAO,MAAM,OAAO,8BAA8B,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,MAAM,GAAG,IAAI,CAWzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,OAEuB;;;;;;;;;;;;;;;;;;;;;;EAoBjC;AAeD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAH7B,OAG6B,EAAE,QAAQ,EAFvC,OAEuC;;;;;;;;;;;;;;EAWjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAFvB,OAEuB;;;;;;EAkBjC;AAED,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,UAA0G,CAAC;AAyDrI,YAA+D,eAAe,GAApE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAiB;AAI9E,YAAkB,QAAQ,GAC1B;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAoB,WAAW,EAApB,OAAO,CAClB;;;;IAAoB,OAAO,EAAhB,OAAO,CAClB;IAAmB,OAAO,EAAf,MAAM,CACnB;CAAA,CAAA;AAZD;;;GAGG;AACH;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAH9B,MAG8B,GAF5B,eAAe,EAAE,CAc7B;AAqBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAFrB,OAEqB;;;;qBAMA,MAAM,EAAE;;;;;;;kBAOX,KAAK,CAAC,eAAe,GAAG;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;;;;WAIxD,QAAQ,EAAE;;;;;;mBAMF,MAAM,EAAE;kBAET,MAAM,EAAE;eACX,MAAM,EAAE;;kBAIL,MAAM,EAAE;EAkDpC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,MAEuB,WAKjC;AAEG,YAA0C,YAAY,GAA5C,UAAU,CAAC,OAAO,WAAW,CAAC,CAAc;AAA1D,6DAA6D;AAE7D;;;;;GAKG;AACH,wBAAgB,aAAa,CAJc,CAAC,SAA9B;IAAE,aAAa,EAAE,MAAM,EAAE,CAAA;CAAK,EAId,QAAQ,EAH3B,CAG2B,GAFzB,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAIvC"}
|
package/dist/cli.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.mts","sourceRoot":"","sources":["../src/cli.mjs"],"names":[],"mappings":"AA4BG,OAAQ,KAAA,EAAE,QAAQ,EAAE,MAAM,aAAa,CACvC;AAMA,YAAkB,UAAU,GAC5B;IAAmB,MAAM,EAAd,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAoB,OAAO,AAA3B,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,KAAK,AAAzB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAoB,KAAK,AAAzB,CACA,EADW,MAAM,CACjB;IAAmC,OAAO,EAA/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjC;IAAoB,OAAO,AAA3B,CACA,EADW,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAqB,UAAU,AAA/B,CACA,EADW,OAAO,CAClB;IAAqB,GAAG,AAAxB,CACA,EADW,OAAO,CAClB;IAAqB,IAAI,AAAzB,CAA0B,EAAf,OAAO,CAAQ;CAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.mts","sourceRoot":"","sources":["../src/cli.mjs"],"names":[],"mappings":"AA4BG,OAAQ,KAAA,EAAE,QAAQ,EAAE,MAAM,aAAa,CACvC;AAMA,YAAkB,UAAU,GAC5B;IAAmB,MAAM,EAAd,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAoB,OAAO,AAA3B,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,KAAK,AAAzB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,IAAI,AAAxB,CACA,EADW,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAoB,KAAK,AAAzB,CACA,EADW,MAAM,CACjB;IAAmC,OAAO,EAA/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjC;IAAoB,OAAO,AAA3B,CACA,EADW,MAAM,CACjB;IAAoB,GAAG,AAAvB,CACA,EADW,MAAM,CACjB;IAAqB,UAAU,AAA/B,CACA,EADW,OAAO,CAClB;IAAqB,GAAG,AAAxB,CACA,EADW,OAAO,CAClB;IAAqB,IAAI,AAAzB,CAA0B,EAAf,OAAO,CAAQ;CAAA,CAAA;AAsH7B;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAFpB,SAAS,MAAM,EAEK,YAoB9B;AAED;;;;;;GAMG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAJnB,SAAS,MAAM,EAII,EAAE,EAAE,MAAuB,EAAE,MAAuB,EAAE,AAHjF,CACA,EADQ;IAAE,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;CAGU,GAF5E,OAAO,CAAC,MAAM,CAAC,CA8G3B"}
|
package/dist/csp.d.mts
CHANGED
|
@@ -9,6 +9,17 @@ export type CspNeeds = {
|
|
|
9
9
|
* @returns {CspNeeds}
|
|
10
10
|
*/
|
|
11
11
|
export declare function cspNeeds(html: unknown): CspNeeds;
|
|
12
|
+
/**
|
|
13
|
+
* Origins of absolute URL literals the document holds without a call or attribute that tells which
|
|
14
|
+
* key they need (`url: "https://tiles.example/{z}/{x}/{y}.png"` handed to a map library, a base URL
|
|
15
|
+
* in a template literal). They may or may not be fetched; `--suggest-csp` lists them so a person
|
|
16
|
+
* decides. Origins already in `needs`, link targets, XML namespace hosts, framework documentation
|
|
17
|
+
* hosts and placeholders (`*.example.com`, localhost) are left out.
|
|
18
|
+
* @param {unknown} html
|
|
19
|
+
* @param {CspNeeds} [needs]
|
|
20
|
+
* @returns {string[]}
|
|
21
|
+
*/
|
|
22
|
+
export declare function mentionedOrigins(html: unknown, needs?: CspNeeds): string[];
|
|
12
23
|
/**
|
|
13
24
|
* A suggestion in the `_meta.ui.csp` shape: only the keys with origins, or null when the document
|
|
14
25
|
* needs none (the restrictive default fits it).
|
package/dist/csp.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csp.d.mts","sourceRoot":"","sources":["../src/csp.mjs"],"names":[],"mappings":"AAUG,YAA2F,QAAQ,GAAzF;IAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAAU;
|
|
1
|
+
{"version":3,"file":"csp.d.mts","sourceRoot":"","sources":["../src/csp.mjs"],"names":[],"mappings":"AAUG,YAA2F,QAAQ,GAAzF;IAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAAU;AA+BtG;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,QAAQ,CAqCpB;AAUD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAJ1B,OAI0B,EAAE,KAAK,AAHzC,CACA,EADQ,QAGkD,GAFhD,MAAM,EAAE,CAgBpB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAHrB,QAGqB,GAFnB,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAKpC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAHnB,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,SAAS,CAGnB,GAFjB,QAAQ,CASpB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAHtB,MAGsB,EAAE,MAAM,EAF9B,MAE8B,WAOxC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAJ1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAIP,EAAE,KAAK,EAHjC,QAGiC,GAF/B,OAAO,CAAC,QAAQ,CAAC,CAW7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAJtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAIX,EAAE,IAAI,EAH5B,OAG4B,GAF1B;IAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAcpD"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lintA2uiMessages, parseA2uiMessages } from './a2ui.mjs';
|
|
2
2
|
export { failureKind, firstJsonRpcFromSse, isUiResource, McpHttpClient, McpStdioClient, snapshotServer } from './client.mjs';
|
|
3
|
-
export { cspNeeds, entryCovers, mergeNeeds, suggestCsp, undeclaredOrigins, unusedEntries } from './csp.mjs';
|
|
3
|
+
export { cspNeeds, entryCovers, mentionedOrigins, mergeNeeds, suggestCsp, undeclaredOrigins, unusedEntries } from './csp.mjs';
|
|
4
4
|
export { analyzeHtml, analyzeTools, analyzeUiMeta, compareListRead, ESCAPE_FUNCTIONS, isHandwritten, postMessageStarCalls, UI_MIME, uiMetaOf, withSinkHosts } from './analyze.mjs';
|
|
5
5
|
export { categorizeDomain, categorizeHost, classifyDomains, DOMAIN_CATEGORIES, parseDomainPattern, sinkFor, sinkScope } from './domains.mjs';
|
|
6
6
|
export { levelOf, lintHtml, lintPackageScan, lintResource, lintSnapshot, lintTools, lintUiMeta } from './lint.mjs';
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mjs"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC7H,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mjs"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC7H,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9H,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,oBAAoB,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnL,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC7I,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/H,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AAGxE,YAA0C,SAAS,GAAzC,OAAO,YAAY,EAAE,SAAS,CAAW;AACnD,YAA0C,MAAM,GAAtC,OAAO,eAAe,EAAE,MAAM,CAAQ;AAChD,YAA0C,MAAM,GAAtC,OAAO,eAAe,EAAE,MAAM,CAAQ;AAChD,YAAgD,YAAY,GAAlD,OAAO,eAAe,EAAE,YAAY,CAAc;AAC5D,YAA4C,QAAQ,GAA1C,OAAO,eAAe,EAAE,QAAQ,CAAU;AACpD,YAAmD,eAAe,GAAxD,OAAO,eAAe,EAAE,eAAe,CAAiB;AAClE,YAAgD,aAAa,GAAnD,OAAO,cAAc,EAAE,aAAa,CAAe;AAC7D,YAAiD,cAAc,GAArD,OAAO,cAAc,EAAE,cAAc,CAAgB;AAC/D,YAAwC,QAAQ,GAAtC,OAAO,WAAW,EAAE,QAAQ,CAAU;AAChD,YAAiD,aAAa,GAApD,OAAO,eAAe,EAAE,aAAa,CAAe;AAC9D,YAAkD,cAAc,GAAtD,OAAO,eAAe,EAAE,cAAc,CAAgB;AAChE,YAAqD,iBAAiB,GAA5D,OAAO,eAAe,EAAE,iBAAiB,CAAmB;AACtE,YAA6C,SAAS,GAA5C,OAAO,eAAe,EAAE,SAAS,CAAW;AACtD,YAAwC,OAAO,GAArC,OAAO,YAAY,EAAE,OAAO,CAAS;AAC/C,YAA8C,aAAa,GAAjD,OAAO,YAAY,EAAE,aAAa,CAAe;AAC3D,YAAyC,QAAQ,GAAvC,OAAO,YAAY,EAAE,QAAQ,CAAU;AACjD,YAA2C,WAAW,GAA5C,OAAO,WAAW,EAAE,WAAW,CAAa;AACtD,YAA2C,WAAW,GAA5C,OAAO,WAAW,EAAE,WAAW,CAAa;AACtD,YAA2C,WAAW,GAA5C,OAAO,WAAW,EAAE,WAAW,CAAa;AACtD,YAA0C,UAAU,GAA1C,OAAO,WAAW,EAAE,UAAU,CAAY;AACpD,YAAsC,IAAI,GAAhC,OAAO,aAAa,EAAE,IAAI,CAAM;AAC1C,YAAuC,KAAK,GAAlC,OAAO,aAAa,EAAE,KAAK,CAAO;AAtB/C;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
|
package/dist/server.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","sourceRoot":"","sources":["../src/server.mjs"],"names":[],"mappings":"AAgBG,OAAQ,KAAA,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CACjD;AAGA,YAAkB,OAAO,GACzB;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,KAAK,EAAb,MAAM,CACjB;IAAmB,WAAW,EAAnB,MAAM,CACjB;IAAoC,WAAW,EAApC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAClC;IAA2D,WAAW,EAA3D;QAAE,YAAY,EAAE,IAAI,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,CACzD;IAA0G,GAAG,EAAlG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAC1G;CAAA,CAAA;AA6BD,wBAAwB;AACxB,eAAO,MAAM,KAAK,EADP,OAAO,
|
|
1
|
+
{"version":3,"file":"server.d.mts","sourceRoot":"","sources":["../src/server.mjs"],"names":[],"mappings":"AAgBG,OAAQ,KAAA,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CACjD;AAGA,YAAkB,OAAO,GACzB;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,KAAK,EAAb,MAAM,CACjB;IAAmB,WAAW,EAAnB,MAAM,CACjB;IAAoC,WAAW,EAApC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAClC;IAA2D,WAAW,EAA3D;QAAE,YAAY,EAAE,IAAI,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,CACzD;IAA0G,GAAG,EAAlG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAC1G;CAAA,CAAA;AA6BD,wBAAwB;AACxB,eAAO,MAAM,KAAK,EADP,OAAO,EAwHjB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAHhC,GAGgC,GAF9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAsCnD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,KAAqB,EAAE,MAAuB,EAAE,AAHxE,CACA,EADQ;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;CAGiB,GAFnE,OAAO,CAAC,IAAI,CAAC,CAuBzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-app-lint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "SARIF findings about what an MCP App declares (CSP domain lists, permissions, tool visibility) and what its HTML does (innerHTML sinks, postMessage to '*', external hosts, data sinks), read from a package, saved results or a live server over HTTP or stdio; suggested CSP; an MCP server mode; A2UI message streams (openUrl schemes, sink hosts, data in URLs, markup in Text).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/analyze.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* its tools, and the HTML of the resources themselves. No network here.
|
|
4
4
|
*/
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { cspNeeds } from './csp.mjs';
|
|
6
|
+
import { cspNeeds, mentionedOrigins } from './csp.mjs';
|
|
7
7
|
import { classifyDomains, sinkFor } from './domains.mjs';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -265,7 +265,10 @@ export function analyzeHtml(html) {
|
|
|
265
265
|
sinkHosts: /** @type {string[]} */ ([]),
|
|
266
266
|
// The origins the document loads from or calls, by the _meta.ui.csp key that must allow them.
|
|
267
267
|
cspNeeds: cspNeeds(text),
|
|
268
|
+
// Absolute URLs in the document whose key is unknown (see mentionedOrigins); hints, never findings.
|
|
269
|
+
cspMentioned: /** @type {string[]} */ ([]),
|
|
268
270
|
};
|
|
271
|
+
result.cspMentioned = mentionedOrigins(text, result.cspNeeds);
|
|
269
272
|
result.externalHosts = hostnames([...text.matchAll(URL_ATTR_RE)].map((m) => m[1] ?? '').filter((v) => /^(https?:)?\/\//i.test(v)));
|
|
270
273
|
result.networkHosts = hostnames([...text.matchAll(NETWORK_CALL_RE)].map((m) => m[1] ?? ''));
|
|
271
274
|
for (const match of text.matchAll(SCRIPT_RE)) {
|
package/src/cli.mjs
CHANGED
|
@@ -152,12 +152,19 @@ const USAGE = [
|
|
|
152
152
|
].join('\n');
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* One block per document: its uri, then each key with the origins it needs
|
|
155
|
+
* One block per document: its uri, then each key with the origins it needs, then the absolute URLs
|
|
156
|
+
* the script holds whose key is unknown (see mentionedOrigins).
|
|
156
157
|
* @param {Record<string, Partial<CspNeeds> | null>} suggestions
|
|
158
|
+
* @param {Record<string, string[]>} mentioned
|
|
157
159
|
*/
|
|
158
|
-
function suggestionsText(suggestions) {
|
|
159
|
-
const blocks = Object.entries(suggestions).map(([uri, csp]) =>
|
|
160
|
-
csp ?
|
|
160
|
+
function suggestionsText(suggestions, mentioned) {
|
|
161
|
+
const blocks = Object.entries(suggestions).map(([uri, csp]) => {
|
|
162
|
+
const lines = csp ? Object.entries(csp).map(([key, origins]) => ` ${key}: ${origins.join(' ')}`) : [];
|
|
163
|
+
const extra = mentioned[uri] ?? [];
|
|
164
|
+
if (extra.length > 0) lines.push(` also in script, key unknown (check whether the UI loads them): ${extra.join(' ')}`);
|
|
165
|
+
else if (!csp) lines.push(' needs no CSP (references no external host)');
|
|
166
|
+
return `${uri}\n${lines.join('\n')}`;
|
|
167
|
+
});
|
|
161
168
|
return blocks.length > 0 ? `${blocks.join('\n')}\n` : 'no UI documents\n';
|
|
162
169
|
}
|
|
163
170
|
|
|
@@ -211,17 +218,24 @@ export async function run(argv, { stdout = process.stdout, stderr = process.stde
|
|
|
211
218
|
const findings = [];
|
|
212
219
|
/** @type {Record<string, Partial<CspNeeds> | null>} */
|
|
213
220
|
const suggestions = {};
|
|
214
|
-
/** @
|
|
215
|
-
const
|
|
221
|
+
/** @type {Record<string, string[]>} */
|
|
222
|
+
const mentioned = {};
|
|
223
|
+
/** @param {string} uri @param {Partial<CspNeeds> | undefined} needs @param {string[] | undefined} [extra] */
|
|
224
|
+
const suggest = (uri, needs, extra) => {
|
|
216
225
|
suggestions[relativeUri(uri)] = needs ? suggestCsp(mergeNeeds([needs])) : null;
|
|
226
|
+
if (extra?.length) mentioned[relativeUri(uri)] = extra;
|
|
217
227
|
};
|
|
218
228
|
/** @param {PackageScan} scan @param {string} root */
|
|
219
229
|
const suggestPackage = (scan, root) => {
|
|
220
|
-
for (const doc of scan.html ?? []) suggest(`${root.replace(/[\\/]+$/, '') || root}/${doc.file}`.replace(/^\.\//, ''), doc.cspNeeds);
|
|
230
|
+
for (const doc of scan.html ?? []) suggest(`${root.replace(/[\\/]+$/, '') || root}/${doc.file}`.replace(/^\.\//, ''), doc.cspNeeds, doc.cspMentioned);
|
|
221
231
|
};
|
|
222
232
|
/** @param {{ contents: readonly any[] }} snapshot */
|
|
223
233
|
const suggestSnapshot = ({ contents }) => {
|
|
224
|
-
for (const item of contents)
|
|
234
|
+
for (const item of contents) {
|
|
235
|
+
if (typeof item.text !== 'string') continue;
|
|
236
|
+
const analysis = analyzeHtml(item.text);
|
|
237
|
+
suggest(item.uri ?? 'resources/read', analysis.cspNeeds, analysis.cspMentioned);
|
|
238
|
+
}
|
|
225
239
|
};
|
|
226
240
|
const timeoutMs = options.timeout;
|
|
227
241
|
|
|
@@ -263,7 +277,8 @@ export async function run(argv, { stdout = process.stdout, stderr = process.stde
|
|
|
263
277
|
if (options.html) {
|
|
264
278
|
const html = await readFile(options.html, 'utf8');
|
|
265
279
|
findings.push(...lintHtml(html, options.html));
|
|
266
|
-
|
|
280
|
+
const analysis = analyzeHtml(html);
|
|
281
|
+
suggest(options.html, analysis.cspNeeds, analysis.cspMentioned);
|
|
267
282
|
}
|
|
268
283
|
if (options.a2ui) {
|
|
269
284
|
findings.push(...lintA2uiMessages(parseA2uiMessages(await readFile(options.a2ui, 'utf8')), options.a2ui));
|
|
@@ -274,7 +289,11 @@ export async function run(argv, { stdout = process.stdout, stderr = process.stde
|
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
if (options.suggestCsp) {
|
|
277
|
-
const output = options.format === 'text' ? suggestionsText(suggestions) : `${JSON.stringify(suggestions, null, 2)}\n`;
|
|
292
|
+
const output = options.format === 'text' ? suggestionsText(suggestions, mentioned) : `${JSON.stringify(suggestions, null, 2)}\n`;
|
|
293
|
+
// The JSON stays a declaration to copy; the hints go to stderr.
|
|
294
|
+
if (options.format !== 'text') {
|
|
295
|
+
for (const [uri, extra] of Object.entries(mentioned)) stderr.write(`mcp-app-lint: ${uri}: the script also holds ${extra.join(', ')}; the key is unknown, check whether the UI loads them\n`);
|
|
296
|
+
}
|
|
278
297
|
if (options.out) await writeFile(options.out, output);
|
|
279
298
|
else stdout.write(output);
|
|
280
299
|
return 0;
|
package/src/csp.mjs
CHANGED
|
@@ -16,7 +16,9 @@ const ELEMENT_RE = /<(script|img|video|audio|source|track|link|iframe|frame|imag
|
|
|
16
16
|
const ATTR_RE = /(?:^|\s)(src|href|xlink:href|poster|srcset|rel)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
17
17
|
// rel values of a <link> that fetch nothing (or nothing the CSP governs).
|
|
18
18
|
const NON_FETCHING_REL_RE = /^(?:canonical|alternate|author|help|license|next|prev|search|me|bookmark|external|dns-prefetch|preconnect)$/i;
|
|
19
|
-
|
|
19
|
+
// CSS only: <style> blocks and style attributes. Script is not searched, where `new URL("https://…")` is not a fetch.
|
|
20
|
+
const STYLE_RE = /<style\b[^>]*>([\s\S]*?)<\/style>|\sstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
21
|
+
const CSS_URL_RE = /(?<![\w$.-])url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)|@import\s+(?:url\(\s*)?["']?((?:https?:)?\/\/[^"')\s;]+)/gi;
|
|
20
22
|
// fetch, sendBeacon, WebSocket, EventSource, XHR open: connect-src.
|
|
21
23
|
const CONNECT_CALL_RE = /(?:\b(?:fetch|sendBeacon)\s*\(|\bnew\s+(?:WebSocket|EventSource)\s*\(|\.open\s*\(\s*["'`][A-Za-z]+["'`]\s*,)\s*["'`]((?:https?|wss?):\/\/[^"'`\s]+)/g;
|
|
22
24
|
// import() and static imports from a URL: script-src.
|
|
@@ -67,7 +69,9 @@ export function cspNeeds(html) {
|
|
|
67
69
|
add(key, attrs.poster);
|
|
68
70
|
for (const candidate of (attrs.srcset ?? '').split(',')) add(key, candidate.trim().split(/\s+/)[0]);
|
|
69
71
|
}
|
|
70
|
-
for (const
|
|
72
|
+
for (const style of text.matchAll(STYLE_RE)) {
|
|
73
|
+
for (const match of (style[1] ?? style[2] ?? style[3] ?? '').matchAll(CSS_URL_RE)) add('resourceDomains', match[1] ?? match[2]);
|
|
74
|
+
}
|
|
71
75
|
for (const match of text.matchAll(CONNECT_CALL_RE)) add('connectDomains', match[1]);
|
|
72
76
|
for (const match of text.matchAll(IMPORT_RE)) add('resourceDomains', match[1]);
|
|
73
77
|
return {
|
|
@@ -77,6 +81,40 @@ export function cspNeeds(html) {
|
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
// Absolute URL literals anywhere in the document, and hosts that name things rather than serve them.
|
|
85
|
+
const URL_LITERAL_RE = /["'`]((?:https?|wss?):\/\/[^"'`\s<>]+)/g;
|
|
86
|
+
const NAMESPACE_HOSTS = new Set(['www.w3.org', 'w3.org', 'json-schema.org', 'purl.org', 'schema.org', 'ns.adobe.com']);
|
|
87
|
+
// Hosts that bundles name in error messages and comments (framework docs, source links), not to fetch from.
|
|
88
|
+
const DOC_HOSTS = new Set(['github.com', 'vuejs.org', 'react.dev', 'reactjs.org', 'legacy.reactjs.org', 'angular.dev', 'angular.io', 'svelte.dev', 'solidjs.com', 'preactjs.com', 'developer.mozilla.org', 'nodejs.org', 'www.npmjs.com', 'npmjs.com', 'modelcontextprotocol.io', 'spec.modelcontextprotocol.io', 'zod.dev', 'tc39.es', 'bugs.chromium.org']);
|
|
89
|
+
const PLACEHOLDER_HOST_RE = /\*|(^|\.)example\.(com|org|net)$|^(localhost|127\.0\.0\.1)$/;
|
|
90
|
+
const LINK_RE = /<a\b[^>]*\shref\s*=\s*["']([^"']+)["']/gi;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Origins of absolute URL literals the document holds without a call or attribute that tells which
|
|
94
|
+
* key they need (`url: "https://tiles.example/{z}/{x}/{y}.png"` handed to a map library, a base URL
|
|
95
|
+
* in a template literal). They may or may not be fetched; `--suggest-csp` lists them so a person
|
|
96
|
+
* decides. Origins already in `needs`, link targets, XML namespace hosts, framework documentation
|
|
97
|
+
* hosts and placeholders (`*.example.com`, localhost) are left out.
|
|
98
|
+
* @param {unknown} html
|
|
99
|
+
* @param {CspNeeds} [needs]
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function mentionedOrigins(html, needs = cspNeeds(html)) {
|
|
103
|
+
const text = String(html ?? '');
|
|
104
|
+
const known = new Set([...needs.connectDomains, ...needs.resourceDomains, ...needs.frameDomains]);
|
|
105
|
+
const links = new Set([...text.matchAll(LINK_RE)].map((m) => originOf(m[1] ?? '')).filter(Boolean));
|
|
106
|
+
/** @type {Set<string>} */
|
|
107
|
+
const mentioned = new Set();
|
|
108
|
+
for (const match of text.matchAll(URL_LITERAL_RE)) {
|
|
109
|
+
const origin = originOf(match[1] ?? '');
|
|
110
|
+
if (!origin || known.has(origin) || links.has(origin)) continue;
|
|
111
|
+
const host = new URL(origin).hostname;
|
|
112
|
+
if (NAMESPACE_HOSTS.has(host) || DOC_HOSTS.has(host) || PLACEHOLDER_HOST_RE.test(host)) continue;
|
|
113
|
+
mentioned.add(origin);
|
|
114
|
+
}
|
|
115
|
+
return [...mentioned].sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
/**
|
|
81
119
|
* A suggestion in the `_meta.ui.csp` shape: only the keys with origins, or null when the document
|
|
82
120
|
* needs none (the restrictive default fits it).
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lintA2uiMessages, parseA2uiMessages } from './a2ui.mjs';
|
|
2
2
|
export { failureKind, firstJsonRpcFromSse, isUiResource, McpHttpClient, McpStdioClient, snapshotServer } from './client.mjs';
|
|
3
|
-
export { cspNeeds, entryCovers, mergeNeeds, suggestCsp, undeclaredOrigins, unusedEntries } from './csp.mjs';
|
|
3
|
+
export { cspNeeds, entryCovers, mentionedOrigins, mergeNeeds, suggestCsp, undeclaredOrigins, unusedEntries } from './csp.mjs';
|
|
4
4
|
export { analyzeHtml, analyzeTools, analyzeUiMeta, compareListRead, ESCAPE_FUNCTIONS, isHandwritten, postMessageStarCalls, UI_MIME, uiMetaOf, withSinkHosts } from './analyze.mjs';
|
|
5
5
|
export { categorizeDomain, categorizeHost, classifyDomains, DOMAIN_CATEGORIES, parseDomainPattern, sinkFor, sinkScope } from './domains.mjs';
|
|
6
6
|
export { levelOf, lintHtml, lintPackageScan, lintResource, lintSnapshot, lintTools, lintUiMeta } from './lint.mjs';
|
package/src/server.mjs
CHANGED
|
@@ -88,12 +88,17 @@ export const TOOLS = [
|
|
|
88
88
|
description:
|
|
89
89
|
'The minimal _meta.ui.csp for an MCP App document: connectDomains for the origins it fetches or opens sockets to, ' +
|
|
90
90
|
'resourceDomains for scripts, styles, fonts, images and media, frameDomains for iframes. Static: a URL the script ' +
|
|
91
|
-
'assembles at run time is not seen.
|
|
91
|
+
'assembles at run time is not seen. csp is null when no origin has a known key; mentioned lists absolute URLs the ' +
|
|
92
|
+
'script holds whose key cannot be told statically (for example a tile URL handed to a map library), to check by hand.',
|
|
92
93
|
inputSchema: { type: 'object', properties: { html: { type: 'string', description: 'The HTML text of the resource.' } }, required: ['html'] },
|
|
93
94
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
94
95
|
async run(args) {
|
|
95
|
-
const
|
|
96
|
-
|
|
96
|
+
const analysis = analyzeHtml(requireString(args, 'html'));
|
|
97
|
+
const csp = suggestCsp(analysis.cspNeeds);
|
|
98
|
+
const mentioned = analysis.cspMentioned;
|
|
99
|
+
const hint = mentioned.length > 0 ? `\nThe script also holds ${mentioned.join(', ')}; the key is unknown. Check whether the UI loads them (a map tile server or a CDN base URL usually belongs in resourceDomains, an API in connectDomains).` : '';
|
|
100
|
+
const text = csp ? `${JSON.stringify({ csp }, null, 2)}${hint}` : mentioned.length > 0 ? `No origin with a known key.${hint}` : 'The document references no external origin; it needs no CSP.';
|
|
101
|
+
return { text, structured: { csp, mentioned } };
|
|
97
102
|
},
|
|
98
103
|
},
|
|
99
104
|
{
|