Skip to content

Commit 75eb24c

Browse files
authored
Merge pull request #3 from MCPJam/claude/review-prs-5OzWa
feat: host-probe MCP app to capture host capabilities & context
2 parents 3cb8cae + 5b8e24f commit 75eb24c

7 files changed

Lines changed: 623 additions & 7 deletions

File tree

host-probe.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<title>MCP Host Probe</title>
8+
</head>
9+
<body>
10+
<main class="probe">
11+
<header class="probe-header">
12+
<h1>MCP Host Probe</h1>
13+
<p id="status" class="probe-status">Initializing...</p>
14+
<div class="probe-actions">
15+
<button id="btn-csp-probes" type="button">Run CSP network probes</button>
16+
<button id="btn-reupload" type="button">Re-upload with latest deltas</button>
17+
</div>
18+
</header>
19+
20+
<section id="section-ui" class="probe-section">
21+
<h2><span>ui/initialize result</span> <button data-copy="ui" type="button">Copy</button></h2>
22+
<pre>(pending)</pre>
23+
</section>
24+
25+
<section id="section-runtime" class="probe-section">
26+
<h2><span>Runtime (sandbox / CSP / navigator)</span> <button data-copy="runtime" type="button">Copy</button></h2>
27+
<pre>(pending)</pre>
28+
</section>
29+
30+
<section id="section-deltas" class="probe-section">
31+
<h2><span>host-context-changed deltas</span> <button data-copy="deltas" type="button">Copy</button></h2>
32+
<pre>(none yet)</pre>
33+
</section>
34+
35+
<section id="section-raw" class="probe-section">
36+
<h2><span>Raw incoming messages (first 20)</span> <button data-copy="raw" type="button">Copy</button></h2>
37+
<pre>(pending)</pre>
38+
</section>
39+
</main>
40+
<script type="module" src="/src/host-probe.ts"></script>
41+
</body>
42+
</html>

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"description": "MCP App that visually explains how tool inputs and outputs drive UI rendering",
77
"scripts": {
88
"build:ui": "cross-env INPUT=mcp-app.html vite build",
9+
"build:probe": "cross-env INPUT=host-probe.html vite build",
910
"build:authorize": "cross-env INPUT=authorize.html vite build",
10-
"build": "npm run build:ui && npm run build:authorize",
11+
"build": "npm run build:ui && npm run build:probe && npm run build:authorize",
1112
"deploy": "npm run build && wrangler deploy",
12-
"dev": "npm run build:ui && wrangler dev",
13+
"dev": "npm run build:ui && npm run build:probe && wrangler dev",
1314
"start": "wrangler dev",
1415
"cf-typegen": "wrangler types"
1516
},

src/host-probe-capture.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { HostProbeSnapshot } from "./host-probe-types";
2+
3+
export function captureRuntime(): HostProbeSnapshot["runtime"] {
4+
let sandboxAttr: string | null = null;
5+
let allowAttr: string | null = null;
6+
let crossOriginBlocked = false;
7+
try {
8+
const fe = window.frameElement as HTMLIFrameElement | null;
9+
if (fe) {
10+
sandboxAttr = fe.getAttribute("sandbox");
11+
allowAttr = fe.getAttribute("allow");
12+
}
13+
} catch {
14+
crossOriginBlocked = true;
15+
}
16+
17+
let metaCsp: string | null = null;
18+
try {
19+
metaCsp =
20+
document
21+
.querySelector('meta[http-equiv="Content-Security-Policy"]')
22+
?.getAttribute("content") ?? null;
23+
} catch {
24+
/* ignore */
25+
}
26+
27+
let permissionsPolicy: string | null = null;
28+
try {
29+
const doc = document as unknown as {
30+
permissionsPolicy?: { allowedFeatures?: () => string[] };
31+
featurePolicy?: { allowedFeatures?: () => string[] };
32+
};
33+
const pp = doc.permissionsPolicy ?? doc.featurePolicy;
34+
if (pp?.allowedFeatures) {
35+
permissionsPolicy = pp.allowedFeatures().join(" ");
36+
}
37+
} catch {
38+
/* ignore */
39+
}
40+
41+
let ancestorOriginsLength = 0;
42+
try {
43+
ancestorOriginsLength = window.location.ancestorOrigins?.length ?? 0;
44+
} catch {
45+
/* ignore */
46+
}
47+
48+
return {
49+
location: {
50+
origin: window.location.origin,
51+
href: window.location.href,
52+
ancestorOriginsLength,
53+
},
54+
navigator: {
55+
userAgent: navigator.userAgent,
56+
platform: navigator.platform,
57+
languages: navigator.languages ?? [],
58+
hardwareConcurrency: navigator.hardwareConcurrency ?? 0,
59+
},
60+
frame: {
61+
sandboxAttr,
62+
allowAttr,
63+
crossOriginBlocked,
64+
doubleIframed: window.parent !== window.top,
65+
},
66+
policies: {
67+
metaCsp,
68+
permissionsPolicy,
69+
},
70+
};
71+
}
72+
73+
export async function runCspProbes(
74+
urls: string[],
75+
): Promise<NonNullable<HostProbeSnapshot["runtime"]["cspProbes"]>> {
76+
const results: NonNullable<HostProbeSnapshot["runtime"]["cspProbes"]> = [];
77+
for (const url of urls) {
78+
try {
79+
await fetch(url, { mode: "no-cors" });
80+
results.push({ url, ok: true, errorName: null });
81+
} catch (e) {
82+
results.push({
83+
url,
84+
ok: false,
85+
errorName: e instanceof Error ? e.name : String(e),
86+
});
87+
}
88+
}
89+
return results;
90+
}
91+
92+
// The `App` SDK exposes `getHostContext()` but not the rest of the
93+
// `ui/initialize` result. Sniff the raw envelope from a parallel
94+
// message log to recover hostInfo / hostCapabilities / protocolVersion.
95+
export function findUiInitializeResult(
96+
messages: Array<{ data: unknown }>,
97+
): {
98+
protocolVersion?: string;
99+
hostInfo?: unknown;
100+
hostCapabilities?: unknown;
101+
} | null {
102+
for (const { data } of messages) {
103+
if (!data || typeof data !== "object") continue;
104+
const result = (data as { result?: unknown }).result;
105+
if (!result || typeof result !== "object") continue;
106+
const r = result as Record<string, unknown>;
107+
if ("hostCapabilities" in r || "hostInfo" in r) {
108+
return {
109+
protocolVersion:
110+
typeof r.protocolVersion === "string" ? r.protocolVersion : undefined,
111+
hostInfo: r.hostInfo,
112+
hostCapabilities: r.hostCapabilities,
113+
};
114+
}
115+
}
116+
return null;
117+
}

src/host-probe-types.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
2+
3+
export interface HostProbeSnapshot {
4+
schemaVersion: 1;
5+
capturedAt: string;
6+
mcp?: {
7+
clientInfo?: unknown;
8+
clientCapabilities?: unknown;
9+
};
10+
uiInitialize: {
11+
protocolVersion?: string;
12+
hostInfo?: unknown;
13+
hostCapabilities?: unknown;
14+
hostContext?: McpUiHostContext;
15+
};
16+
runtime: {
17+
location: {
18+
origin: string;
19+
href: string;
20+
ancestorOriginsLength: number;
21+
};
22+
navigator: {
23+
userAgent: string;
24+
platform: string;
25+
languages: readonly string[];
26+
hardwareConcurrency: number;
27+
};
28+
frame: {
29+
sandboxAttr: string | null;
30+
allowAttr: string | null;
31+
crossOriginBlocked: boolean;
32+
doubleIframed: boolean;
33+
};
34+
policies: {
35+
metaCsp: string | null;
36+
permissionsPolicy: string | null;
37+
};
38+
cspProbes?: Array<{ url: string; ok: boolean; errorName: string | null }>;
39+
};
40+
deltas: Array<{ at: string; hostContext: Partial<McpUiHostContext> }>;
41+
errors: Array<{ where: string; message: string }>;
42+
}

src/host-probe.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
:root {
2+
color-scheme: light dark;
3+
}
4+
5+
.probe {
6+
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
7+
color: var(--color-text-primary, #111);
8+
background: var(--color-background-primary, #fff);
9+
padding: 16px;
10+
display: flex;
11+
flex-direction: column;
12+
gap: 12px;
13+
min-height: 100vh;
14+
box-sizing: border-box;
15+
}
16+
17+
.probe-header {
18+
display: flex;
19+
flex-direction: column;
20+
gap: 8px;
21+
}
22+
23+
.probe-header h1 {
24+
margin: 0;
25+
font-size: 18px;
26+
font-weight: 600;
27+
}
28+
29+
.probe-status {
30+
margin: 0;
31+
font-size: 12px;
32+
color: var(--color-text-secondary, #555);
33+
}
34+
35+
.probe-actions {
36+
display: flex;
37+
gap: 8px;
38+
flex-wrap: wrap;
39+
}
40+
41+
.probe-actions button {
42+
padding: 6px 10px;
43+
font-size: 12px;
44+
border-radius: var(--border-radius-sm, 6px);
45+
border: 1px solid var(--color-border-primary, #ccc);
46+
background: var(--color-background-secondary, #f5f5f5);
47+
color: inherit;
48+
cursor: pointer;
49+
}
50+
51+
.probe-actions button:disabled {
52+
opacity: 0.5;
53+
cursor: default;
54+
}
55+
56+
.probe-section {
57+
border: 1px solid var(--color-border-secondary, #ddd);
58+
border-radius: var(--border-radius-md, 8px);
59+
padding: 10px;
60+
background: var(--color-background-secondary, transparent);
61+
}
62+
63+
.probe-section h2 {
64+
margin: 0 0 8px;
65+
font-size: 13px;
66+
font-weight: 600;
67+
display: flex;
68+
align-items: center;
69+
justify-content: space-between;
70+
}
71+
72+
.probe-section button {
73+
font-size: 11px;
74+
padding: 2px 8px;
75+
border-radius: 4px;
76+
border: 1px solid var(--color-border-primary, #ccc);
77+
background: transparent;
78+
cursor: pointer;
79+
color: inherit;
80+
}
81+
82+
.probe-section pre {
83+
margin: 0;
84+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
85+
font-size: 11px;
86+
white-space: pre-wrap;
87+
word-break: break-word;
88+
max-height: 400px;
89+
overflow: auto;
90+
}

0 commit comments

Comments
 (0)