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 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`, images (`src`, `srcset`, `poster`) and media;
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
- the document references no external origin and the restrictive default fits it. MCPAPP001 carries
64
- the same suggestion in its message and in `properties.suggestedCsp`.
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
 
@@ -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.
@@ -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;;EAmDjC;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"}
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"}
@@ -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;AA+G7B;;;;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,CAkG3B"}
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).
@@ -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;AA6BtG;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,QAAQ,CAmCpB;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"}
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mjs"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mjs"],"names":[],"mappings":"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"}
@@ -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,EAmHjB,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"}
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.7",
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 ? `${uri}\n${Object.entries(csp).map(([key, origins]) => ` ${key}: ${origins.join(' ')}`).join('\n')}` : `${uri}\n needs no CSP (references no external host)`);
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
- /** @param {string} uri @param {Partial<CspNeeds> | undefined} needs */
215
- const suggest = (uri, needs) => {
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) if (typeof item.text === 'string') suggest(item.uri ?? 'resources/read', analyzeHtml(item.text).cspNeeds);
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
- suggest(options.html, analyzeHtml(html).cspNeeds);
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
- const CSS_URL_RE = /url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)|@import\s+["']((?:https?:)?\/\/[^"']+)/gi;
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 match of text.matchAll(CSS_URL_RE)) add('resourceDomains', match[1] ?? match[2]);
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. Returns null when the document references no external origin.',
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 csp = suggestCsp(analyzeHtml(requireString(args, 'html')).cspNeeds);
96
- return { text: csp ? JSON.stringify({ csp }, null, 2) : 'The document references no external origin; it needs no CSP.', structured: { csp } };
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
  {