mcp-app-lint 0.1.3 → 0.1.4
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 +2 -2
- package/dist/analyze.d.mts +6 -1
- package/dist/analyze.d.mts.map +1 -1
- package/package.json +2 -2
- package/src/analyze.mjs +10 -9
- package/src/lint.mjs +1 -1
- package/src/rules.mjs +2 -2
package/README.md
CHANGED
|
@@ -70,8 +70,8 @@ npx mcp-app-lint --dir . --format sarif --out mcp-app-lint.sarif --fail-on error
|
|
|
70
70
|
| MCPAPP014 | html-form-action | note | a form that posts somewhere |
|
|
71
71
|
| MCPAPP015 | html-external-host | note | scripts, styles, images or media from external hosts |
|
|
72
72
|
| MCPAPP016 | html-sink-host | error | a URL in the document points at a sink host |
|
|
73
|
-
| MCPAPP017 | meta-list-read-mismatch | warning | `resources/read`
|
|
74
|
-
| MCPAPP018 | meta-read-wider-than-list | error | the read-time
|
|
73
|
+
| MCPAPP017 | meta-list-read-mismatch | warning | `resources/read` and `resources/list` both declare a field (`csp`, `permissions`, `domain`) with different values; a listing with no CSP is not a mismatch, since the SDK puts the CSP in the read result |
|
|
74
|
+
| MCPAPP018 | meta-read-wider-than-list | error | both declare a CSP and the read-time one adds an entry (domain or wildcard) the listing did not declare under the same key |
|
|
75
75
|
| MCPAPP019 | csp-sink-tenant-host | note | one bucket, worker or app on a storage or serverless service from the denylist (`acct.blob.core.windows.net`, `pub-<id>.r2.dev`); fine while the owner keeps the name |
|
|
76
76
|
|
|
77
77
|
Hand-written means an inline script with short lines and not too many of them; bundles carry
|
package/dist/analyze.d.mts
CHANGED
|
@@ -36,18 +36,23 @@ export declare function analyzeUiMeta(meta: unknown): {
|
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Does `resources/read` declare a different policy than `resources/list`? The host that checked list is then wrong.
|
|
39
|
+
* `mismatch` is any difference, for the census. A listing may stay silent: the MCP Apps SDK puts the CSP in the read
|
|
40
|
+
* result only. So `conflict` and `readWider`, which the rules use, compare a field (csp, permissions, domain) only
|
|
41
|
+
* when both sides declare it; `readWider` is a CSP entry at read time that the listing's same key does not have.
|
|
39
42
|
* @param {unknown} listMeta `_meta` of the listing entry
|
|
40
43
|
* @param {unknown} readMeta `_meta` of the read result's content item
|
|
41
44
|
*/
|
|
42
45
|
export declare function compareListRead(listMeta: unknown, readMeta: unknown): {
|
|
43
46
|
comparable: boolean;
|
|
44
47
|
mismatch: boolean;
|
|
45
|
-
|
|
48
|
+
conflict: boolean;
|
|
49
|
+
readWider: boolean;
|
|
46
50
|
listOnly?: undefined;
|
|
47
51
|
readOnly?: undefined;
|
|
48
52
|
} | {
|
|
49
53
|
comparable: boolean;
|
|
50
54
|
mismatch: boolean;
|
|
55
|
+
conflict: boolean;
|
|
51
56
|
readWider: boolean;
|
|
52
57
|
listOnly: boolean;
|
|
53
58
|
readOnly: boolean;
|
package/dist/analyze.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAgBG,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
|
|
1
|
+
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAgBG,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;EAiDjC;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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-app-lint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=22.12"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@render-policy/core": "^0.1.
|
|
32
|
+
"@render-policy/core": "^0.1.4",
|
|
33
33
|
"eslint": "^10.11.0",
|
|
34
34
|
"eslint-plugin-render-policy": "^0.1.2"
|
|
35
35
|
},
|
package/src/analyze.mjs
CHANGED
|
@@ -80,22 +80,23 @@ function canonical(value) {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Does `resources/read` declare a different policy than `resources/list`? The host that checked list is then wrong.
|
|
83
|
+
* `mismatch` is any difference, for the census. A listing may stay silent: the MCP Apps SDK puts the CSP in the read
|
|
84
|
+
* result only. So `conflict` and `readWider`, which the rules use, compare a field (csp, permissions, domain) only
|
|
85
|
+
* when both sides declare it; `readWider` is a CSP entry at read time that the listing's same key does not have.
|
|
83
86
|
* @param {unknown} listMeta `_meta` of the listing entry
|
|
84
87
|
* @param {unknown} readMeta `_meta` of the read result's content item
|
|
85
88
|
*/
|
|
86
89
|
export function compareListRead(listMeta, readMeta) {
|
|
87
90
|
const a = uiMetaOf(listMeta);
|
|
88
91
|
const b = uiMetaOf(readMeta);
|
|
89
|
-
if (!a && !b) return { comparable: false, mismatch: false };
|
|
92
|
+
if (!a && !b) return { comparable: false, mismatch: false, conflict: false, readWider: false };
|
|
93
|
+
const FIELDS = /** @type {const} */ (['csp', 'permissions', 'domain']);
|
|
90
94
|
/** @param {UiMeta | null} m */
|
|
91
|
-
const pick = (m) => (m ?
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const readWider = (() => {
|
|
95
|
-
|
|
96
|
-
return CSP_KEYS.some((k) => (b.csp[k]?.length ?? 0) > (a.csp[k]?.length ?? 0) || (b.csp[k] ?? []).some((/** @type {unknown} */ d) => String(d).includes('*') && !(a.csp[k] ?? []).includes(d)));
|
|
97
|
-
})();
|
|
98
|
-
return { comparable: true, mismatch: left !== right, readWider, listOnly: !!a && !b, readOnly: !a && !!b };
|
|
95
|
+
const pick = (m) => (m ? Object.fromEntries(FIELDS.map((f) => [f, m[f] ?? null])) : null);
|
|
96
|
+
const same = (/** @type {unknown} */ x, /** @type {unknown} */ y) => JSON.stringify(canonical(x)) === JSON.stringify(canonical(y));
|
|
97
|
+
const conflict = FIELDS.some((f) => a?.[f] != null && b?.[f] != null && !same(a[f], b[f]));
|
|
98
|
+
const readWider = !!(a?.csp && b?.csp) && CSP_KEYS.some((k) => (b.csp[k] ?? []).some((/** @type {unknown} */ d) => !(a.csp[k] ?? []).includes(d)));
|
|
99
|
+
return { comparable: true, mismatch: !same(pick(a), pick(b)), conflict, readWider, listOnly: !!a && !b, readOnly: !a && !!b };
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
package/src/lint.mjs
CHANGED
|
@@ -125,7 +125,7 @@ export function lintResource({ uri, listMeta = null, readMeta = null, html = nul
|
|
|
125
125
|
if (listMeta && readMeta) {
|
|
126
126
|
const compared = compareListRead(listMeta, readMeta);
|
|
127
127
|
if (compared.readWider) findings.push(finding('MCPAPP018', 'resources/read declares a wider policy than resources/list.', uri));
|
|
128
|
-
else if (compared.
|
|
128
|
+
else if (compared.conflict) findings.push(finding('MCPAPP017', 'resources/read and resources/list declare different policies.', uri));
|
|
129
129
|
}
|
|
130
130
|
if (analysis) findings.push(...htmlFindings(analysis, uri));
|
|
131
131
|
return findings;
|
package/src/rules.mjs
CHANGED
|
@@ -25,8 +25,8 @@ export const RULES = [
|
|
|
25
25
|
{ id: 'MCPAPP014', name: 'html-form-action', level: 'note', short: 'Form posts to a URL', full: "The specification's CSP has no form-action directive; a form is a way out of the sandbox for data. Check the destination." },
|
|
26
26
|
{ id: 'MCPAPP015', name: 'html-external-host', level: 'note', short: 'Document references external hosts', full: 'Scripts, styles, images or media are loaded from hosts outside the document. They must be declared in resourceDomains or the host blocks them.' },
|
|
27
27
|
{ id: 'MCPAPP016', name: 'html-sink-host', level: 'error', short: 'Document references a data sink', full: 'A URL in the document points at a service on the sink denylist.' },
|
|
28
|
-
{ id: 'MCPAPP017', name: 'meta-list-read-mismatch', level: 'warning', short: 'resources/read declares a different policy than resources/list', full: 'Hosts must prefer the read-time policy.
|
|
29
|
-
{ id: 'MCPAPP018', name: 'meta-read-wider-than-list', level: 'error', short: 'resources/read widens the policy seen at resources/list', full: '
|
|
28
|
+
{ id: 'MCPAPP017', name: 'meta-list-read-mismatch', level: 'warning', short: 'resources/read declares a different policy than resources/list', full: 'Hosts must prefer the read-time policy. Reported when both sides declare the same field (csp, permissions or domain) with different values and the read-time CSP adds nothing (that is MCPAPP018). A listing that declares no CSP is not a mismatch: the MCP Apps SDK puts the CSP in the read result only.' },
|
|
29
|
+
{ id: 'MCPAPP018', name: 'meta-read-wider-than-list', level: 'error', short: 'resources/read widens the policy seen at resources/list', full: 'Both sides declare a CSP and the read-time one has an entry the listing does not have under the same key (a new domain, a replaced one, or a wildcard). This is the pattern a host that reviews at connection time cannot see.' },
|
|
30
30
|
{ id: 'MCPAPP019', name: 'csp-sink-tenant-host', level: 'note', short: 'CSP entry names one account on a multi-tenant service', full: 'The host is a single bucket, worker or app on a storage or serverless service from the sink denylist (acct.blob.core.windows.net, pub-<id>.r2.dev). It reaches only that account, which is fine while the owner keeps it; a deleted bucket or app name can be registered by someone else.' },
|
|
31
31
|
];
|
|
32
32
|
|