diff --git a/.gitignore b/.gitignore index 928ba0687..879a4f263 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,5 @@ coverage/ a2a_agents/python/a2ui_agent/src/a2ui/assets/**/*.json ## new agent SDK path agent_sdks/python/src/a2ui/assets/**/*.json -## Generated JS file from the strictly-typed `sandbox.ts`. -samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.js +## Generated JS file from the strictly-typed `sandbox.ts`. +samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.js diff --git a/samples/agent/adk/orchestrator/README.md b/samples/agent/adk/orchestrator/README.md index 0b8b442b0..b0d79efc2 100644 --- a/samples/agent/adk/orchestrator/README.md +++ b/samples/agent/adk/orchestrator/README.md @@ -42,30 +42,17 @@ Subagents are configured using RemoteA2aAgent which translates ADK events to A2A uv run . --port=10005 ``` - Optionally, run the MCP Server and MCP App Proxy Agent to MCP Apps in A2UI demo: - - ```bash - cd samples/agent/mcp/calculator - uv run . --port=8000 - ``` - - ```bash - cd samples/agent/adk/mcp_app_proxy - uv run . --port=10006 - ``` - 3. Run the orchestrator agent: ```bash cd samples/agent/adk/orchestrator - uv run . --port=10002 --subagent_urls=http://localhost:10003 --subagent_urls=http://localhost:10004 --subagent_urls=http://localhost:10005 --subagent_urls=http://localhost:10006 + uv run . --port=10002 --subagent_urls=http://localhost:10003 --subagent_urls=http://localhost:10004 --subagent_urls=http://localhost:10005 ``` 4. Try commands that work with any agent: a. "Who is Alex Jordan?" (routed to contact lookup agent) b. "Show me chinese food restaurants in NYC" (routed to restaurant finder agent) c. "Show my sales data for Q4" (routed to rizzcharts) - d. "Open calculator" (routed to mcp app proxy) ## Disclaimer @@ -75,4 +62,4 @@ All operational data received from an external agent—including its AgentCard, Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites. -Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. \ No newline at end of file +Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. diff --git a/samples/client/angular/README.md b/samples/client/angular/README.md index b68a240cd..9b3e3a273 100644 --- a/samples/client/angular/README.md +++ b/samples/client/angular/README.md @@ -37,6 +37,7 @@ Here are the instructions if you want to do each step manually. * `npm start -- contact` * `npm start -- rizzcharts` * `npm start -- orchestrator` + * `npm run build:sandbox && npm start -- mcp_calculator` * `npm start -- gallery` (Client-only, no server required) 5. Open http://localhost:4200/ diff --git a/samples/client/angular/angular.json b/samples/client/angular/angular.json index 87be99fb3..7044ec54d 100644 --- a/samples/client/angular/angular.json +++ b/samples/client/angular/angular.json @@ -495,6 +495,116 @@ } } } + }, + "mcp_calculator": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "projects/mcp_calculator", + "sourceRoot": "projects/mcp_calculator/src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "projects/mcp_calculator/src/main.ts", + "tsConfig": "projects/mcp_calculator/tsconfig.app.json", + "preserveSymlinks": true, + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "projects/mcp_calculator/public" + } + ], + "styles": [ + "projects/mcp_calculator/src/styles.scss" + ], + "server": "projects/mcp_calculator/src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "projects/mcp_calculator/src/server.ts" + } + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500KB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "mcp_calculator:build:production" + }, + "development": { + "buildTarget": "mcp_calculator:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "codeCoverage": true, + "tsConfig": "projects/mcp_calculator/tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "projects/mcp_calculator/public" + } + ], + "styles": [ + "projects/mcp_calculator/src/styles.scss" + ] + } + } + } } }, "cli": { diff --git a/samples/client/angular/package.json b/samples/client/angular/package.json index c17b2d97c..db5af3a29 100644 --- a/samples/client/angular/package.json +++ b/samples/client/angular/package.json @@ -14,11 +14,7 @@ "build:renderer": "cd ../../../renderers && for dir in 'web_core' 'markdown/markdown-it'; do (cd \"$dir\" && npm install && npm run build); done", "serve:agent:restaurant": "cd ../../agent/adk/restaurant_finder && uv run .", "demo:restaurant": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\"", - "serve:agent:contact": "cd ../../agent/adk/contact_lookup && uv run .", - "demo:contact": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:contact\" \"npm start -- contact\"", - "serve:agent:rizzcharts": "cd ../../agent/adk/rizzcharts && uv run .", - "demo:rizzcharts": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:rizzcharts\" \"npm start -- rizzcharts\"", - "build:sandbox": "esbuild projects/orchestrator/public/sandbox_iframe/sandbox.ts --bundle --outfile=projects/orchestrator/public/sandbox_iframe/sandbox.js --format=esm --platform=browser" + "build:sandbox": "esbuild projects/mcp_calculator/public/sandbox_iframe/sandbox.ts --bundle --outfile=projects/mcp_calculator/public/sandbox_iframe/sandbox.js --format=esm --platform=browser" }, "prettier": { "printWidth": 100, @@ -93,4 +89,4 @@ "../../../renderers/web_core", "../../../renderers/markdown/markdown-it" ] -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/mcp_calculator/README.md b/samples/client/angular/projects/mcp_calculator/README.md new file mode 100644 index 000000000..25e0c7ea5 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/README.md @@ -0,0 +1,23 @@ +# MCP Calculator + +Sample application using the Chat-Canvas component with MCP Calculator Agent. + +## Prerequisites + +1. [nodejs](https://nodejs.org/en) +2. An endpoint hosting the MCP Calculator A2AService. ([Review the instructions on how to run MCP Calculator A2AService](../../../../agent/adk/mcp_app_proxy/README.md).) + +## Running + +1. Build the shared dependencies by running `npm run build` in the `renderers/web_core` directory +2. Install the dependencies: `npm i` +3. Run the A2A server for all of the agents. ([Link to instructions](../../../../agent/adk/mcp_app_proxy/README.md)) +4. Build the `sandbox.js` for testing MCP Apps in A2UI demo + +- `npm run build:sandbox` + +5. Run the app: + +- `npm start -- mcp_calculator` + +6. Open http://localhost:4200/ diff --git a/samples/client/angular/projects/mcp_calculator/public/favicon.ico b/samples/client/angular/projects/mcp_calculator/public/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/samples/client/angular/projects/mcp_calculator/public/favicon.ico differ diff --git a/samples/client/angular/projects/mcp_calculator/public/gemini-color.svg b/samples/client/angular/projects/mcp_calculator/public/gemini-color.svg new file mode 100644 index 000000000..f1cf35757 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/public/gemini-color.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/samples/client/angular/projects/orchestrator/public/sandbox_iframe/README.md b/samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/README.md similarity index 90% rename from samples/client/angular/projects/orchestrator/public/sandbox_iframe/README.md rename to samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/README.md index 71a93370b..f2e2f98c6 100644 --- a/samples/client/angular/projects/orchestrator/public/sandbox_iframe/README.md +++ b/samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/README.md @@ -6,7 +6,7 @@ This directory contains the `sandbox.html` and its associated resources. `sandbox.html` is designed to be loaded into an `` to provide a secure, isolated environment for running MCP (Model Context Protocol) applications. It -acts as a bridge between the host application (Orchestrator) and the untrusted +acts as a bridge between the host application (MCP Calculator) and the untrusted or external MCP apps, managing communication via `postMessage`. ## Development diff --git a/samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.html b/samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.html similarity index 100% rename from samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.html rename to samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.html diff --git a/samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.ts b/samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.ts similarity index 100% rename from samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.ts rename to samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.ts diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts new file mode 100644 index 000000000..89eb8f792 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Catalog } from '@a2ui/angular'; +import { inputBinding } from '@angular/core'; + +export const DEMO_CATALOG = { + McpApp: { + type: () => import('./mcp-app').then((r) => r.McpApp), + bindings: ({ properties }) => [ + inputBinding( + 'content', + () => ('content' in properties && properties['content']) || undefined, + ), + inputBinding('title', () => ('title' in properties && properties['title']) || undefined), + ], + }, +} as Catalog; diff --git a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/mcp-app.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts similarity index 96% rename from samples/client/angular/projects/orchestrator/src/a2ui-catalog/mcp-app.ts rename to samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts index 898bb887a..311a50965 100644 --- a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/mcp-app.ts +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts @@ -145,7 +145,7 @@ export class McpApp // Set src to trigger load AFTER listener is ready // TODO: Make the sandbox URL configurable. To ensure CORS encapsulation, the sandbox - // should be served from a different origin than the orchestrator. + // should be served from a different origin than the host app. const sandboxUrl = 'sandbox_iframe/sandbox.html'; this.iframeSrc.set( this.sanitizer.bypassSecurityTrustResourceUrl(sandboxUrl), @@ -164,7 +164,7 @@ export class McpApp const emptyMcpClient = null; const bridge = new AppBridge( emptyMcpClient, - { name: 'A2UI Orchestrator', version: '1.0.0' }, + { name: 'MCP Calculator', version: '1.0.0' }, { openLinks: {}, logging: {}, @@ -206,8 +206,8 @@ export class McpApp // Pseudo-code for dispatch: // const actionName = params.name; // if (this.allowedTools().includes(actionName)) { - // // Dispatch action to A2UI orchestrator/store - // // events.dispatch('a2ui.action', { name: actionName, ... }); + // // Dispatch action to host store + // // events.dispatch('host.action', { name: actionName, ... }); // return { content: [{ type: "text", text: "Action dispatched" }] }; // } else { // console.warn(`Tool '${actionName}' blocked.`); diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts new file mode 100644 index 000000000..125e66bed --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; +import { provideServerRendering, withRoutes } from '@angular/ssr'; +import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; + +const serverConfig: ApplicationConfig = { + providers: [provideServerRendering(withRoutes(serverRoutes))], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts new file mode 100644 index 000000000..60835c163 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + configureChatCanvasFeatures, + usingA2aService, + usingA2uiRenderers, + usingDefaultSanitizerMarkdownRenderer, +} from '@a2a_chat_canvas/config'; +import { + ApplicationConfig, + provideBrowserGlobalErrorListeners, + provideZonelessChangeDetection, +} from '@angular/core'; +import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; +import { DEMO_CATALOG } from '../a2ui-catalog/catalog'; +import { A2aServiceImpl } from '../services/a2a-service-impl'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZonelessChangeDetection(), + provideClientHydration(withEventReplay()), + configureChatCanvasFeatures( + usingA2aService(A2aServiceImpl), + usingA2uiRenderers(DEMO_CATALOG), + usingDefaultSanitizerMarkdownRenderer(), + ), + ], +}; diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.html b/samples/client/angular/projects/mcp_calculator/src/app/app.html new file mode 100644 index 000000000..66f19d83c --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.html @@ -0,0 +1,42 @@ + + + + + + + + + + + {{ agentName() }} + + + + + + I summon a calculator app served from an MCP server over A2UI. + + + + calculate + Open Calculator + + + + + + diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts new file mode 100644 index 000000000..6e22a0113 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender, + }, +]; diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.scss b/samples/client/angular/projects/mcp_calculator/src/app/app.scss new file mode 100644 index 000000000..dff7732af --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.scss @@ -0,0 +1,80 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +main { + height: 100vh; +} + +.empty-history { + display: block; + padding-bottom: 8rem; + padding-top: 2rem; + text-align: center; +} + +.empty-history-text { + margin: 0; + font: var(--mat-sys-display-small); + margin-bottom: 2rem; +} + +.agent-header { + display: flex; + align-items: center; + flex-flow: row wrap; + justify-content: center; + margin-bottom: 1rem; +} + +.agent-header-part { + margin-inline-end: 1.25rem; + margin-block: 8px; + font-size: 1.5rem; +} + +.agent-name { + background: linear-gradient( + 90deg, + #217bfe 28.03%, + #078efb 49.56%, + #ac87eb 71.1% + ); + background-clip: text; + color: transparent; +} + +.suggestion-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin-top: 32px; +} + +.chip { + // Override Material button styles to look like a chip + border-radius: 100px !important; // Force pill shape + padding: 10px 16px !important; + display: inline-flex !important; + align-items: center !important; + height: auto !important; + line-height: 25px !important; // Match icon height + .material-icons-outlined { + font-size: 20px; + margin-right: 8px; + line-height: 1; // Prevent icon from affecting line height + position: relative; + top: 4px; // Move icon down slightly to match text + } +} \ No newline at end of file diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.ts new file mode 100644 index 000000000..2f91c641d --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { A2aChatCanvas } from '@a2a_chat_canvas/a2a-chat-canvas'; +import { ChatService } from '@a2a_chat_canvas/services/chat-service'; +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.html', + styleUrl: './app.scss', + imports: [A2aChatCanvas, MatButtonModule], + + changeDetection: ChangeDetectionStrategy.Eager, +}) +export class App { + + protected readonly agentName = signal('MCP Calculator'); + private readonly chatService = inject(ChatService); + + + sendMessage(text: string) { + this.chatService.sendMessage(text); + } +} diff --git a/samples/client/angular/projects/mcp_calculator/src/index.html b/samples/client/angular/projects/mcp_calculator/src/index.html new file mode 100644 index 000000000..1c8f79462 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/index.html @@ -0,0 +1,33 @@ + + + + + + + MCP Calculator + + + + + + + + + diff --git a/samples/client/angular/projects/mcp_calculator/src/main.server.ts b/samples/client/angular/projects/mcp_calculator/src/main.server.ts new file mode 100644 index 000000000..fcfe120c8 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/main.server.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; +import { App } from './app/app'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); + +export default bootstrap; diff --git a/samples/client/angular/projects/mcp_calculator/src/main.ts b/samples/client/angular/projects/mcp_calculator/src/main.ts new file mode 100644 index 000000000..3c4480110 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/main.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bootstrapApplication } from '@angular/platform-browser'; +import { App } from './app/app'; +import { appConfig } from './app/app.config'; + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/samples/client/angular/projects/mcp_calculator/src/server.ts b/samples/client/angular/projects/mcp_calculator/src/server.ts new file mode 100644 index 000000000..3a15c8ec2 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/server.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MessageSendParams, Part, SendMessageResponse } from '@a2a-js/sdk'; +import { A2AClient } from '@a2a-js/sdk/client'; +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; +import express from 'express'; +import { join } from 'node:path'; +import { v4 as uuidv4 } from 'uuid'; + +const browserDistFolder = join(import.meta.dirname, '../browser'); +const app = express(); +const angularApp = new AngularNodeAppEngine(); +let client: A2AClient | null = null; + +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }), +); + +app.post('/a2a', (req, res) => { + let originalBody = ''; + + req.on('data', (chunk) => { + originalBody += chunk.toString(); + }); + + req.on('end', async () => { + const data = JSON.parse(originalBody); + + console.log('[a2a-middleware] Received data:', data); + + const parts: Part[] = data['parts']; + + const sendParams: MessageSendParams = { + message: { + messageId: uuidv4(), + role: 'user', + parts, + kind: 'message', + metadata: { + a2uiClientCapabilities: { + supportedCatalogIds: [ + 'https://a2ui.org/specification/v0_8/standard_catalog_definition.json', + 'a2ui.org:a2ui/v0.8/mcp_app_catalog.json', + ], + }, + }, + }, + }; + + let client: A2AClient; + try { + client = await createOrGetClient(); + } catch (error) { + res.status(500).json({ error: 'Failed to create A2A client.' }); + return; + } + + let response: SendMessageResponse; + try { + response = await client.sendMessage(sendParams); + } catch (error) { + res.status(500).json({ error: 'Failed to send message.' }); + return; + } + + res.set('Cache-Control', 'no-store'); + + if ('error' in response) { + res.status(500).json({ error: JSON.stringify(response.error) }); + return; + } + + res.json(response); + }); +}); + +app.get('/a2a/agent-card', async (req, res) => { + try { + const response = await fetchWithCustomHeader( + 'http://localhost:10006/.well-known/agent-card.json', + ); + if (!response.ok) { + res.status(response.status).json({ error: 'Failed to fetch agent card' }); + return; + } + const card = await response.json(); + res.json(card); + } catch (error) { + console.error('Error fetching agent card:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.use((req, res, next) => { + angularApp + .handle(req) + .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .catch(next); +}); + +if (isMainModule(import.meta.url) || process.env['pm_id']) { + const port = process.env['PORT'] || 4000; + app.listen(port, (error) => { + if (error) { + throw error; + } + + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +async function fetchWithCustomHeader(url: string | URL | Request, init?: RequestInit) { + const headers = new Headers(init?.headers); + headers.set('X-A2A-Extensions', 'https://a2ui.org/a2a-extension/a2ui/v0.8'); + const newInit = { ...init, headers }; + return fetch(url, newInit); +} + +async function createOrGetClient() { + // Create a client pointing to the agent's Agent Card URL. + client ??= await A2AClient.fromCardUrl('http://localhost:10006/.well-known/agent-card.json', { + fetchImpl: fetchWithCustomHeader, + }); + + return client; +} + +function isJson(str: string): boolean { + try { + const parsed = JSON.parse(str); + return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed); + } catch (err) { + console.warn(err); + return false; + } +} + +export const reqHandler = createNodeRequestHandler(app); diff --git a/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts b/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts new file mode 100644 index 000000000..55ac70497 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentCard, Part, SendMessageSuccessResponse } from '@a2a-js/sdk'; +import { A2aService } from '@a2a_chat_canvas/interfaces/a2a-service'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class A2aServiceImpl implements A2aService { + + async sendMessage(parts: Part[], signal?: AbortSignal): Promise { + const response = await fetch('/a2a', { + body: JSON.stringify({ parts: parts }), + method: 'POST', + signal, + }); + + if (response.ok) { + const data = await response.json(); + return data; + } + + const error = (await response.json()) as { error: string }; + throw new Error(error.error); + } + + async getAgentCard(): Promise { + const response = await fetch('/a2a/agent-card'); + if (!response.ok) { + throw new Error('Failed to fetch agent card'); + } + const card = await response.json() as AgentCard; + return card; + } +} diff --git a/samples/client/angular/projects/mcp_calculator/src/styles.scss b/samples/client/angular/projects/mcp_calculator/src/styles.scss new file mode 100644 index 000000000..e6c8f2610 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/src/styles.scss @@ -0,0 +1,204 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@use '@angular/material' as mat; + + +@mixin styled-scrollbar { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + background: transparent; + } + + ::-webkit-scrollbar-track { + background: var(--mat-sys-container-low); + border-radius: 0.25rem; + } + + ::-webkit-scrollbar-thumb { + background: var(--mat-sys-outline-variant); + border-radius: 0.25rem; + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--mat-sys-outline); + } +} + +html { + color-scheme: light dark; + @include mat.theme(( + color: mat.$blue-palette, + typography: Roboto, + density: 0 + )); + + @include styled-scrollbar; +} +:root { + --n-100: #ffffff; + --n-99: #fcfcfc; + --n-98: #f9f9f9; + --n-95: #f1f1f1; + --n-90: #e2e2e2; + --n-80: #c6c6c6; + --n-70: #ababab; + --n-60: #919191; + --n-50: #777777; + --n-40: #5e5e5e; + --n-35: #525252; + --n-30: #474747; + --n-25: #3b3b3b; + --n-20: #303030; + --n-15: #262626; + --n-10: #1b1b1b; + --n-5: #111111; + --n-0: #000000; + + --p-100: #ffffff; + --p-99: #fffbff; + --p-98: #fcf8ff; + --p-95: #f2efff; + --p-90: #e1e0ff; + --p-80: #c0c1ff; + --p-70: #a0a3ff; + --p-60: #8487ea; + --p-50: #6a6dcd; + --p-40: #5154b3; + --p-35: #4447a6; + --p-30: #383b99; + --p-25: #2c2e8d; + --p-20: #202182; + --p-15: #131178; + --p-10: #06006c; + --p-5: #03004d; + --p-0: #000000; + + --s-100: #ffffff; + --s-99: #fffbff; + --s-98: #fcf8ff; + --s-95: #f2efff; + --s-90: #e2e0f9; + --s-80: #c6c4dd; + --s-70: #aaa9c1; + --s-60: #8f8fa5; + --s-50: #75758b; + --s-40: #5d5c72; + --s-35: #515165; + --s-30: #454559; + --s-25: #393a4d; + --s-20: #2e2f42; + --s-15: #242437; + --s-10: #191a2c; + --s-5: #0f0f21; + --s-0: #000000; + + --t-100: #ffffff; + --t-99: #fffbff; + --t-98: #fff8f9; + --t-95: #ffecf4; + --t-90: #ffd8ec; + --t-80: #e9b9d3; + --t-70: #cc9eb8; + --t-60: #af849d; + --t-50: #946b83; + --t-40: #79536a; + --t-35: #6c475d; + --t-30: #5f3c51; + --t-25: #523146; + --t-20: #46263a; + --t-15: #3a1b2f; + --t-10: #2e1125; + --t-5: #22071a; + --t-0: #000000; + + --nv-100: #ffffff; + --nv-99: #fffbff; + --nv-98: #fcf8ff; + --nv-95: #f2effa; + --nv-90: #e4e1ec; + --nv-80: #c8c5d0; + --nv-70: #acaab4; + --nv-60: #918f9a; + --nv-50: #777680; + --nv-40: #5e5d67; + --nv-35: #52515b; + --nv-30: #46464f; + --nv-25: #3b3b43; + --nv-20: #303038; + --nv-15: #25252d; + --nv-10: #1b1b23; + --nv-5: #101018; + --nv-0: #000000; + + --e-100: #ffffff; + --e-99: #fffbff; + --e-98: #fff8f7; + --e-95: #ffedea; + --e-90: #ffdad6; + --e-80: #ffb4ab; + --e-70: #ff897d; + --e-60: #ff5449; + --e-50: #de3730; + --e-40: #ba1a1a; + --e-35: #a80710; + --e-30: #93000a; + --e-25: #7e0007; + --e-20: #690005; + --e-15: #540003; + --e-10: #410002; + --e-5: #2d0001; + --e-0: #000000; + + --primary: #137fec; + --text-color: #fff; + --background-light: #f6f7f8; + --background-dark: #101922; + --border-color: oklch(from var(--background-light) l c h / calc(alpha * 0.15)); + --elevated-background-light: oklch(from var(--background-light) l c h / calc(alpha * 0.05)); + --bb-grid-size: 4px; + --bb-grid-size-2: calc(var(--bb-grid-size) * 2); + --bb-grid-size-3: calc(var(--bb-grid-size) * 3); + --bb-grid-size-4: calc(var(--bb-grid-size) * 4); + --bb-grid-size-5: calc(var(--bb-grid-size) * 5); + --bb-grid-size-6: calc(var(--bb-grid-size) * 6); + --bb-grid-size-7: calc(var(--bb-grid-size) * 7); + --bb-grid-size-8: calc(var(--bb-grid-size) * 8); + --bb-grid-size-9: calc(var(--bb-grid-size) * 9); + --bb-grid-size-10: calc(var(--bb-grid-size) * 10); + --bb-grid-size-11: calc(var(--bb-grid-size) * 11); + --bb-grid-size-12: calc(var(--bb-grid-size) * 12); + --bb-grid-size-13: calc(var(--bb-grid-size) * 13); + --bb-grid-size-14: calc(var(--bb-grid-size) * 14); + --bb-grid-size-15: calc(var(--bb-grid-size) * 15); + --bb-grid-size-16: calc(var(--bb-grid-size) * 16); +} + +* { + box-sizing: border-box; +} + +html, +body { + --font-family: 'Google Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-family-flex: 'Google Sans Flex', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-family-mono: 'Google Sans Code', 'Helvetica Neue', Helvetica, Arial, sans-serif; + + font-family: var(--font-family); + margin: 0; + padding: 0; + width: 100svw; + height: 100svh; +} \ No newline at end of file diff --git a/samples/client/angular/projects/mcp_calculator/tsconfig.app.json b/samples/client/angular/projects/mcp_calculator/tsconfig.app.json new file mode 100644 index 000000000..3175e5ae7 --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/tsconfig.app.json @@ -0,0 +1,17 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json b/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json new file mode 100644 index 000000000..0feea88ed --- /dev/null +++ b/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/samples/client/angular/projects/orchestrator/README.md b/samples/client/angular/projects/orchestrator/README.md index afcff6a95..85789a8ea 100644 --- a/samples/client/angular/projects/orchestrator/README.md +++ b/samples/client/angular/projects/orchestrator/README.md @@ -16,12 +16,8 @@ This angular app connects to an Orchastrator Agent which takes user messages and 2. Build the shared dependencies by running `npm run build` in the `renderers/web_core` directory 3. Install the dependencies: `npm i` 4. Run the A2A server for all of the agents. ([Link to instructions](../../../../agent/adk/orchestrator/README.md)) -5. Optionally, build the `sandbox.js` for testing MCP Apps in A2UI demo - -- `npm run build:sandbox` - -6. Run the app: +5. Run the app: - `npm start -- orchestrator` -7. Open http://localhost:4200/ +6. Open http://localhost:4200/ diff --git a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts index e6720ddeb..9d13204e7 100644 --- a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts +++ b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts @@ -38,14 +38,4 @@ export const DEMO_CATALOG = { inputBinding('title', () => ('title' in properties && properties['title']) || undefined), ], }, - McpApp: { - type: () => import('./mcp-app').then((r) => r.McpApp), - bindings: ({ properties }) => [ - inputBinding( - 'content', - () => ('content' in properties && properties['content']) || undefined, - ), - inputBinding('title', () => ('title' in properties && properties['title']) || undefined), - ], - }, } as Catalog; diff --git a/samples/client/angular/projects/orchestrator/src/server.ts b/samples/client/angular/projects/orchestrator/src/server.ts index 71817669e..8db1068e8 100644 --- a/samples/client/angular/projects/orchestrator/src/server.ts +++ b/samples/client/angular/projects/orchestrator/src/server.ts @@ -63,7 +63,6 @@ app.post('/a2a', (req, res) => { a2uiClientCapabilities: { supportedCatalogIds: [ 'https://a2ui.org/specification/v0_8/standard_catalog_definition.json', - 'a2ui.org:a2ui/v0.8/mcp_app_catalog.json', ], }, },
+ I summon a calculator app served from an MCP server over A2UI. +