Skip to content

Commit c0dca1f

Browse files
mattdhollowayCopilot
authored andcommitted
Adopt MCP Apps 2026-01-26 view-side capabilities
* Declare appCapabilities.availableDisplayModes (defaults to ["inline"]) during initialization, as required by the new spec. * Track McpUiHostContext (and its updates via onhostcontextchanged) and thread it into AppProvider, which now picks up host-supplied theme + CSS style variables and projects them onto the root element so Primer components inherit host theming. * Add setModelContext and openLink helpers to useMcpApp. issue-write and pr-write call setModelContext on a successful submission so the agent has the new entity in its next-turn context; get-me uses openLink for the profile's external blog link. The pinned @modelcontextprotocol/ext-apps ^1.7.2 was already resolved to 1.7.2 in the lockfile, so no dependency bump is required for the new HostContext / openLink / updateModelContext APIs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 69f786b commit c0dca1f

5 files changed

Lines changed: 187 additions & 54 deletions

File tree

ui/src/apps/get-me/App.tsx

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { StrictMode, useState } from "react";
2+
import type React from "react";
23
import { createRoot } from "react-dom/client";
34
import { Avatar, Box, Text, Link, Heading, Spinner } from "@primer/react";
45
import {
@@ -62,8 +63,20 @@ function AvatarWithFallback({ src, login, size }: { src?: string; login: string;
6263
);
6364
}
6465

65-
function UserCard({ user }: { user: UserData }) {
66+
function UserCard({
67+
user,
68+
onOpenLink,
69+
}: {
70+
user: UserData;
71+
onOpenLink?: (url: string) => void;
72+
}) {
6673
const d = user.details || {};
74+
const handleClick =
75+
onOpenLink &&
76+
((url: string) => (e: React.MouseEvent) => {
77+
e.preventDefault();
78+
onOpenLink(url);
79+
});
6780

6881
return (
6982
<Box
@@ -103,7 +116,13 @@ function UserCard({ user }: { user: UserData }) {
103116
{d.blog && (
104117
<>
105118
<Box sx={{ color: "fg.muted" }}><LinkIcon size={16} /></Box>
106-
<Link href={d.blog} target="_blank">{d.blog}</Link>
119+
<Link
120+
href={d.blog}
121+
target="_blank"
122+
onClick={handleClick?.(d.blog)}
123+
>
124+
{d.blog}
125+
</Link>
107126
</>
108127
)}
109128
{d.email && (
@@ -140,41 +159,39 @@ function UserCard({ user }: { user: UserData }) {
140159
}
141160

142161
function GetMeApp() {
143-
const { error, toolResult } = useMcpApp({
162+
const { error, toolResult, hostContext, openLink } = useMcpApp({
144163
appName: "github-mcp-server-get-me",
145164
});
146165

147-
if (error) {
148-
return <Text sx={{ color: "danger.fg" }}>Error: {error.message}</Text>;
149-
}
150-
151-
if (!toolResult) {
152-
return (
153-
<Box display="flex" alignItems="center" gap={2}>
154-
<Spinner size="small" />
155-
<Text sx={{ color: "fg.muted" }}>Loading user data...</Text>
156-
</Box>
157-
);
158-
}
159-
160-
// Parse user data from tool result
161-
const textContent = toolResult.content?.find((c: { type: string }) => c.type === "text");
162-
if (!textContent || !("text" in textContent)) {
163-
return <Text sx={{ color: "danger.fg" }}>No user data in response</Text>;
164-
}
166+
const content = (() => {
167+
if (error) {
168+
return <Text sx={{ color: "danger.fg" }}>Error: {error.message}</Text>;
169+
}
170+
if (!toolResult) {
171+
return (
172+
<Box display="flex" alignItems="center" gap={2}>
173+
<Spinner size="small" />
174+
<Text sx={{ color: "fg.muted" }}>Loading user data...</Text>
175+
</Box>
176+
);
177+
}
178+
const textContent = toolResult.content?.find((c: { type: string }) => c.type === "text");
179+
if (!textContent || !("text" in textContent)) {
180+
return <Text sx={{ color: "danger.fg" }}>No user data in response</Text>;
181+
}
182+
try {
183+
const userData = JSON.parse(textContent.text as string) as UserData;
184+
return <UserCard user={userData} onOpenLink={(url) => void openLink(url)} />;
185+
} catch {
186+
return <Text sx={{ color: "danger.fg" }}>Failed to parse user data</Text>;
187+
}
188+
})();
165189

166-
try {
167-
const userData = JSON.parse(textContent.text as string) as UserData;
168-
return <UserCard user={userData} />;
169-
} catch {
170-
return <Text sx={{ color: "danger.fg" }}>Failed to parse user data</Text>;
171-
}
190+
return <AppProvider hostContext={hostContext}>{content}</AppProvider>;
172191
}
173192

174193
createRoot(document.getElementById("root")!).render(
175194
<StrictMode>
176-
<AppProvider>
177-
<GetMeApp />
178-
</AppProvider>
195+
<GetMeApp />
179196
</StrictMode>
180197
);

ui/src/apps/issue-write/App.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function CreateIssueApp() {
121121
const [error, setError] = useState<string | null>(null);
122122
const [successIssue, setSuccessIssue] = useState<IssueResult | null>(null);
123123

124-
const { app, error: appError, toolInput, callTool } = useMcpApp({
124+
const { app, error: appError, toolInput, callTool, hostContext, setModelContext } = useMcpApp({
125125
appName: "github-mcp-server-issue-write",
126126
});
127127

@@ -181,6 +181,19 @@ function CreateIssueApp() {
181181
try {
182182
const issueData = JSON.parse(textContent.text as string);
183183
setSuccessIssue(issueData);
184+
// Per the MCP Apps 2026-01-26 spec, push the created/updated issue
185+
// into the model's context so subsequent agent turns have it.
186+
void setModelContext({
187+
structuredContent: issueData,
188+
content: [
189+
{
190+
type: "text",
191+
text: isUpdateMode
192+
? `Issue #${issueNumber} in ${owner}/${repo} was updated by the user via the issue-write view.`
193+
: `A new issue was created in ${owner}/${repo} by the user via the issue-write view.`,
194+
},
195+
],
196+
});
184197
} catch {
185198
setSuccessIssue({ title, body });
186199
}
@@ -191,8 +204,9 @@ function CreateIssueApp() {
191204
} finally {
192205
setIsSubmitting(false);
193206
}
194-
}, [title, body, owner, repo, isUpdateMode, issueNumber, callTool]);
207+
}, [title, body, owner, repo, isUpdateMode, issueNumber, callTool, setModelContext]);
195208

209+
const body_node = (() => {
196210
if (appError) {
197211
return (
198212
<Flash variant="danger" sx={{ m: 2 }}>
@@ -307,12 +321,13 @@ function CreateIssueApp() {
307321
</Box>
308322
</Box>
309323
);
324+
})();
325+
326+
return <AppProvider hostContext={hostContext}>{body_node}</AppProvider>;
310327
}
311328

312329
createRoot(document.getElementById("root")!).render(
313330
<StrictMode>
314-
<AppProvider>
315-
<CreateIssueApp />
316-
</AppProvider>
331+
<CreateIssueApp />
317332
</StrictMode>
318333
);

ui/src/apps/pr-write/App.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function CreatePRApp() {
126126
const [isDraft, setIsDraft] = useState(false);
127127
const [maintainerCanModify, setMaintainerCanModify] = useState(true);
128128

129-
const { app, error: appError, toolInput, callTool } = useMcpApp({
129+
const { app, error: appError, toolInput, callTool, hostContext, setModelContext } = useMcpApp({
130130
appName: "github-mcp-server-create-pull-request",
131131
});
132132

@@ -175,26 +175,37 @@ function CreatePRApp() {
175175
if (textContent && textContent.type === "text" && textContent.text) {
176176
const prData = JSON.parse(textContent.text);
177177
setSuccessPR(prData);
178+
// Push the new PR into the model context so subsequent agent
179+
// turns can reference it (MCP Apps 2026-01-26 ui/update-model-context).
180+
void setModelContext({
181+
structuredContent: prData,
182+
content: [
183+
{
184+
type: "text",
185+
text: `A new pull request was created in ${owner}/${repo} by the user via the create-pull-request view.`,
186+
},
187+
],
188+
});
178189
}
179190
}
180191
} catch (e) {
181192
setError(e instanceof Error ? e.message : "An error occurred");
182193
} finally {
183194
setIsSubmitting(false);
184195
}
185-
}, [title, body, owner, repo, head, base, isDraft, maintainerCanModify, callTool]);
196+
}, [title, body, owner, repo, head, base, isDraft, maintainerCanModify, callTool, setModelContext]);
186197

187198
if (successPR) {
188199
return (
189-
<AppProvider>
200+
<AppProvider hostContext={hostContext}>
190201
<SuccessView pr={successPR} owner={owner} repo={repo} submittedTitle={submittedTitle} />
191202
</AppProvider>
192203
);
193204
}
194205

195206
if (!app && !appError) {
196207
return (
197-
<AppProvider>
208+
<AppProvider hostContext={hostContext}>
198209
<Box display="flex" alignItems="center" justifyContent="center" p={4}>
199210
<Spinner size="medium" />
200211
</Box>
@@ -204,14 +215,14 @@ function CreatePRApp() {
204215

205216
if (appError) {
206217
return (
207-
<AppProvider>
218+
<AppProvider hostContext={hostContext}>
208219
<Flash variant="danger">{appError.message}</Flash>
209220
</AppProvider>
210221
);
211222
}
212223

213224
return (
214-
<AppProvider>
225+
<AppProvider hostContext={hostContext}>
215226
<Box
216227
borderWidth={1}
217228
borderStyle="solid"

ui/src/components/AppProvider.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,50 @@
11
import { ThemeProvider, BaseStyles, Box } from "@primer/react";
2-
import type { ReactNode } from "react";
3-
import { useEffect } from "react";
2+
import type { ReactNode, CSSProperties } from "react";
3+
import { useEffect, useMemo } from "react";
4+
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
45
import { FeedbackFooter } from "./FeedbackFooter";
56

67
interface AppProviderProps {
78
children: ReactNode;
9+
hostContext?: McpUiHostContext;
810
}
911

10-
export function AppProvider({ children }: AppProviderProps) {
12+
export function AppProvider({ children, hostContext }: AppProviderProps) {
13+
const hostTheme = hostContext?.theme;
14+
const hostVariables = hostContext?.styles?.variables;
15+
1116
useEffect(() => {
12-
// Set up theme data attributes for proper Primer theming
13-
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
14-
const colorMode = prefersDark ? "dark" : "light";
17+
// Prefer the host-supplied theme; fall back to the OS preference.
18+
const colorMode =
19+
hostTheme === "light" || hostTheme === "dark"
20+
? hostTheme
21+
: window.matchMedia("(prefers-color-scheme: dark)").matches
22+
? "dark"
23+
: "light";
1524
document.body.setAttribute("data-color-mode", colorMode);
1625
document.body.setAttribute("data-light-theme", "light");
1726
document.body.setAttribute("data-dark-theme", "dark");
18-
}, []);
27+
}, [hostTheme]);
28+
29+
// Project the host's standardized CSS variables onto the root so child
30+
// components can consume them via `var(--color-...)`. We rely on Primer's
31+
// own defaults when the host does not supply variables.
32+
const styleVars = useMemo<CSSProperties | undefined>(() => {
33+
if (!hostVariables) return undefined;
34+
const out: Record<string, string> = {};
35+
for (const [key, value] of Object.entries(hostVariables)) {
36+
if (typeof value === "string") out[key] = value;
37+
}
38+
return out as CSSProperties;
39+
}, [hostVariables]);
40+
41+
const colorMode =
42+
hostTheme === "light" || hostTheme === "dark" ? hostTheme : "auto";
1943

2044
return (
21-
<ThemeProvider colorMode="auto">
45+
<ThemeProvider colorMode={colorMode}>
2246
<BaseStyles>
23-
<Box p={3}>
47+
<Box p={3} style={styleVars}>
2448
{children}
2549
<FeedbackFooter />
2650
</Box>

0 commit comments

Comments
 (0)