diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 29bf31f7b53..b95656f2fa1 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -93,6 +93,7 @@ "@angular-devkit/architect": "^0.1402.2", "@angular-devkit/core": "^14.2.2", "@google/events": "^5.1.1", + "@modelcontextprotocol/ext-apps": "^1.3.2", "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/body-parser": "^1.17.0", @@ -2961,9 +2962,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -3701,13 +3702,40 @@ "integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==", "dev": true }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.5.0.tgz", + "integrity": "sha512-q4fut89TOoP2LEPHSGfZErIf1K1xOTTzV+41h/bB2BqKw2gKb0uLKbHusOy1UtbY0puS16zBho/vFp3f5XMVbQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", - "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -3715,14 +3743,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -10029,10 +10058,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, @@ -10040,7 +10072,16 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" } }, "node_modules/express/node_modules/cookie": { @@ -12200,11 +12241,10 @@ } }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -21989,9 +22029,9 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -24124,9 +24164,9 @@ } }, "@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "requires": {} }, "@humanwhocodes/config-array": { @@ -24570,12 +24610,19 @@ "integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==", "dev": true }, + "@modelcontextprotocol/ext-apps": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.5.0.tgz", + "integrity": "sha512-q4fut89TOoP2LEPHSGfZErIf1K1xOTTzV+41h/bB2BqKw2gKb0uLKbHusOy1UtbY0puS16zBho/vFp3f5XMVbQ==", + "dev": true, + "requires": {} + }, "@modelcontextprotocol/sdk": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", - "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "requires": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -24583,14 +24630,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "dependencies": { "accepts": { @@ -29277,10 +29325,19 @@ } }, "express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "requires": {} + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "requires": { + "ip-address": "10.1.0" + }, + "dependencies": { + "ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==" + } + } }, "extend": { "version": "3.0.2", @@ -30811,10 +30868,9 @@ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, "hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", - "peer": true + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==" }, "hosted-git-info": { "version": "2.8.9", @@ -37958,9 +38014,9 @@ } }, "zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==" + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" }, "zod-to-json-schema": { "version": "3.24.5", diff --git a/package.json b/package.json index 63833f427d1..6a01ad8516e 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "@angular-devkit/architect": "^0.1402.2", "@angular-devkit/core": "^14.2.2", "@google/events": "^5.1.1", + "@modelcontextprotocol/ext-apps": "^1.3.2", "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/body-parser": "^1.17.0", diff --git a/src/experiments.ts b/src/experiments.ts index e8bbcdf3b2a..abf45e19094 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -184,6 +184,11 @@ export const ALL_EXPERIMENTS = experiments({ default: false, public: true, }, + mcpapps: { + shortDescription: "Enables MCP Apps features", + fullDescription: "Enables MCP Apps features, including returning UI resource URIs.", + public: true, + }, fdcift: { shortDescription: "Enable instrumentless trial for SQL Connect", default: true, diff --git a/src/mcp/apps/update_environment/mcp-app.html b/src/mcp/apps/update_environment/mcp-app.html new file mode 100644 index 00000000000..d09722e1f8b --- /dev/null +++ b/src/mcp/apps/update_environment/mcp-app.html @@ -0,0 +1,299 @@ + + + + + + Update Firebase Environment + + + + + + +
+
+

Choose a Firebase Project

+

Select an active Firebase project for your workspace.

+
+ +
+

Current Context

+

Project ID: -

+

User: -

+
+ + + + + +
+ +
+
+
+ + + diff --git a/src/mcp/apps/update_environment/mcp-app.ts b/src/mcp/apps/update_environment/mcp-app.ts new file mode 100644 index 00000000000..ab878f8a635 --- /dev/null +++ b/src/mcp/apps/update_environment/mcp-app.ts @@ -0,0 +1,155 @@ +import { + App, + applyDocumentTheme, + applyHostStyleVariables, + applyHostFonts, +} from "@modelcontextprotocol/ext-apps"; + +const app = new App({ name: "Update Firebase Environment", version: "1.0.0" }); + +const projectListContainer = document.getElementById("project-list") as HTMLDivElement; +const searchInput = document.getElementById("search-input") as HTMLInputElement; +const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement; +const statusBox = document.getElementById("status-box") as HTMLDivElement; + +let projects: any[] = []; +let filteredProjects: any[] = []; +let selectedProjectId: string | null = null; + +const envProjectIdEl = document.getElementById("env-project-id") as HTMLSpanElement; +const envUserEl = document.getElementById("env-user") as HTMLSpanElement; + +function showStatus(message: string, type: "success" | "error" | "info") { + statusBox.textContent = message; + statusBox.className = `status ${type}`; + statusBox.style.display = "block"; +} + +function renderProjects() { + projectListContainer.innerHTML = ""; + + if (filteredProjects.length === 0) { + projectListContainer.innerHTML = ` + + `; + return; + } + + filteredProjects.forEach((p) => { + const item = document.createElement("div"); + item.className = "dropdown-item"; + if (p.projectId === selectedProjectId) { + item.classList.add("selected"); + } + + const displayName = p.displayName || p.projectId; + const projectId = p.projectId; + + item.innerHTML = ` +
${displayName}
+
${projectId}
+ `; + + item.onclick = () => { + selectedProjectId = projectId; + submitBtn.disabled = false; + renderProjects(); // Re-render to show selection + }; + + projectListContainer.appendChild(item); + }); +} + +searchInput.oninput = () => { + const query = searchInput.value.toLowerCase().trim(); + if (query === "") { + filteredProjects = projects; + } else { + filteredProjects = projects.filter((p) => { + const name = (p.displayName || p.projectId).toLowerCase(); + const id = p.projectId.toLowerCase(); + return name.includes(query) || id.includes(query); + }); + } + renderProjects(); +}; + +submitBtn.onclick = async () => { + if (!selectedProjectId) return; + + submitBtn.disabled = true; + showStatus(`Updating active project to ${selectedProjectId}...`, "info"); + + try { + const result = await app.callServerTool({ + name: "firebase_update_environment", + arguments: { active_project: selectedProjectId }, + }); + + const textContent = result.content?.find((c: any) => c.type === "text"); + const text = textContent ? (textContent as any).text : "Update complete."; + + if (result.isError) { + showStatus(text, "error"); + submitBtn.disabled = false; + } else { + showStatus(text, "success"); + } + } catch (err: any) { + showStatus(`Error updating environment: ${err.message}`, "error"); + submitBtn.disabled = false; + } +}; + +app.onhostcontextchanged = (ctx: any) => { + if (ctx.theme) applyDocumentTheme(ctx.theme); + if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables); + if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts); + if (ctx.safeAreaInsets) { + const { top, right, bottom, left } = ctx.safeAreaInsets; + document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`; + } +}; + +(async () => { + try { + await app.connect(); + showStatus("Connecting to server...", "info"); + + // Fetch current environment + try { + const envResult = await app.callServerTool({ + name: "firebase_get_environment", + arguments: {}, + }); + const envData = envResult.structuredContent as any; + if (envData) { + envProjectIdEl.textContent = envData.projectId || ""; + envUserEl.textContent = envData.authenticatedUser || ""; + } + } catch (err: any) { + console.error("Failed to fetch environment:", err); + showStatus(`Failed to fetch environment: ${err.message}`, "error"); + } + + // Fetch projects on load + const result = await app.callServerTool({ name: "firebase_list_projects", arguments: {} }); + const data = result.structuredContent as any; + + if (data && data.projects) { + projects = data.projects; + filteredProjects = projects; + renderProjects(); + showStatus("Projects loaded successfully.", "success"); + setTimeout(() => { + if (statusBox.className === "status success") statusBox.style.display = "none"; + }, 3000); + } else { + showStatus("No projects returned from server.", "error"); + } + } catch (err: any) { + showStatus(`Failed to load projects: ${err.message}`, "error"); + } +})(); diff --git a/src/mcp/resources/index.ts b/src/mcp/resources/index.ts index 5a007ceafd2..7578743c415 100644 --- a/src/mcp/resources/index.ts +++ b/src/mcp/resources/index.ts @@ -13,6 +13,10 @@ import { ServerResource, ServerResourceTemplate } from "../resource"; import { trackGA4 } from "../../track"; import { crashlytics_issues } from "./guides/crashlytics_issues"; import { crashlytics_reports } from "./guides/crashlytics_reports"; +// import { login_ui } from "./login_ui"; +import { update_environment_ui } from "./update_environment_ui"; +// import { deploy_ui } from "./deploy_ui"; +// import { init_ui } from "./init_ui"; export const resources = [ app_id, @@ -25,6 +29,10 @@ export const resources = [ init_firestore_rules, init_auth, init_hosting, + // login_ui, + update_environment_ui, + // deploy_ui, + // init_ui, ]; export const resourceTemplates = [docs]; diff --git a/src/mcp/resources/update_environment_ui.ts b/src/mcp/resources/update_environment_ui.ts new file mode 100644 index 00000000000..76f10d3265e --- /dev/null +++ b/src/mcp/resources/update_environment_ui.ts @@ -0,0 +1,32 @@ +import { resource } from "../resource"; +import * as path from "path"; +import * as fs from "fs/promises"; + +import { RESOURCE_MIME_TYPE } from "../util"; +const resourceUri = "ui://core/update_environment/mcp-app.html"; + +export const update_environment_ui = resource( + { + uri: resourceUri, + name: "Update Environment UI", + description: "Visual interface for selecting active Firebase project", + mimeType: RESOURCE_MIME_TYPE, + }, + async () => { + try { + const htmlPath = path.join(__dirname, "../apps/update_environment/mcp-app.html"); + const html = await fs.readFile(htmlPath, "utf-8"); + return { + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + }, + ], + }; + } catch (e: any) { + throw new Error(`Failed to load Update Environment UI: ${e.message}`); + } + }, +); diff --git a/src/mcp/util.spec.ts b/src/mcp/util.spec.ts index 1344c950725..14ba93604c5 100644 --- a/src/mcp/util.spec.ts +++ b/src/mcp/util.spec.ts @@ -1,5 +1,8 @@ import { expect } from "chai"; -import { cleanSchema } from "./util"; +import * as sinon from "sinon"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import * as experiments from "../experiments"; +import { cleanSchema, applyAppMeta } from "./util"; interface TestCase { desc: string; @@ -474,3 +477,33 @@ describe("cleanSchema", () => { }); }); }); + +describe("applyAppMeta", () => { + let experimentsStub: sinon.SinonStub; + + beforeEach(() => { + experimentsStub = sinon.stub(experiments, "isEnabled"); + }); + + afterEach(() => { + experimentsStub.restore(); + }); + + it("should add _meta if mcpapps experiment is enabled", () => { + experimentsStub.withArgs("mcpapps").returns(true); + const result: CallToolResult = { content: [{ type: "text", text: "hello" }] }; + const uri = "ui://test"; + const expected = { + content: [{ type: "text", text: "hello" }], + _meta: { ui: { resourceUri: uri } }, + }; + expect(applyAppMeta(result, uri)).to.deep.equal(expected); + }); + + it("should NOT add _meta if mcpapps experiment is disabled", () => { + experimentsStub.withArgs("mcpapps").returns(false); + const result: CallToolResult = { content: [{ type: "text", text: "hello" }] }; + const uri = "ui://test"; + expect(applyAppMeta(result, uri)).to.deep.equal(result); + }); +}); diff --git a/src/mcp/util.ts b/src/mcp/util.ts index 4d7afc2f1d9..fe3c198b1ca 100644 --- a/src/mcp/util.ts +++ b/src/mcp/util.ts @@ -1,5 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { dump } from "js-yaml"; +import * as experiments from "../experiments"; import { ServerFeature } from "./types"; import { apphostingOrigin, @@ -45,6 +46,27 @@ export function toContent( } as CallToolResult & { structuredContent: any }; } +/** + * Conditionally adds MCP App metadata (_meta.ui.resourceUri) to a CallToolResult. + */ +export function applyAppMeta( + result: CallToolResult, + resourceUri: string, +): CallToolResult & { _meta?: { ui?: { resourceUri: string } } } { + if (experiments.isEnabled("mcpapps")) { + return { + ...result, + _meta: { + ...result._meta, + ui: { + resourceUri, + }, + }, + }; + } + return result; +} + /** * Returns an error message to the user. */ @@ -304,3 +326,5 @@ export function cleanSchema(schema: Record): Record { const result = deepClean(schema, true); // Pass true for isRootLevel return result === null ? {} : result; } + +export const RESOURCE_MIME_TYPE = "application/vnd.mcp.ext-app+html";