diff --git a/package-lock.json b/package-lock.json index d5d0e294..947eb6ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -916,6 +916,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@google/gemini-cli-examples": { + "resolved": "packages/examples", + "link": true + }, "node_modules/@google/genai": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.9.0.tgz", @@ -5657,6 +5661,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -9190,6 +9207,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -10490,6 +10517,26 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11860,6 +11907,17 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "packages/examples": { + "name": "@google/gemini-cli-examples", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.15.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.16.2" + } + }, "packages/vscode-ide-companion": { "name": "gemini-cli-vscode-ide-companion", "version": "0.0.1", diff --git a/packages/examples/background_agent/README.md b/packages/examples/background_agent/README.md new file mode 100644 index 00000000..0e5fd6dc --- /dev/null +++ b/packages/examples/background_agent/README.md @@ -0,0 +1,16 @@ +# Demo Background Agent + +A pretend background agent that does not actually process tasks in the background. Configure in your settings.json with: + +```javascript + "backgroundAgents": { + "demo-background-agent": { + "command": "npm", + "args": [ + "run", + "start:demo-background-agent", + "--workspace=@google/gemini-cli-examples" + ] + } + }, +``` diff --git a/packages/examples/background_agent/demo-background-agent.ts b/packages/examples/background_agent/demo-background-agent.ts new file mode 100644 index 00000000..9ac568f4 --- /dev/null +++ b/packages/examples/background_agent/demo-background-agent.ts @@ -0,0 +1,217 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +const BackgroundAgentMessageSchema = z.object({ + role: z.enum(['user', 'agent']), + parts: z.array(z.any()), +}); + +const BackgroundAgentTaskStatusSchema = z.object({ + state: z.enum([ + 'submitted', + 'working', + 'input-required', + 'completed', + 'canceled', + 'failed', + ]), + message: BackgroundAgentMessageSchema.optional(), +}); + +const BackgroundAgentTaskSchema = z.object({ + id: z.string(), + status: BackgroundAgentTaskStatusSchema, + history: z.array(BackgroundAgentMessageSchema).optional(), +}); +type BackgroundAgentTask = z.infer; + +const server = new McpServer({ + name: 'demo-background-agent', + version: '1.0.0', +}); + +const idToTask = new Map(); + +server.registerTool( + 'startTask', + { + title: 'Start a new task', + description: 'Launches a new task asynchronously.', + inputSchema: { prompt: BackgroundAgentMessageSchema }, + outputSchema: BackgroundAgentTaskSchema.shape, + }, + ({ prompt }) => { + const task: BackgroundAgentTask = { + id: crypto.randomUUID(), + status: { + state: 'submitted', + message: prompt, + }, + history: [], + }; + + idToTask.set(task.id, task); + + return { + content: [], + structuredContent: task, + }; + }, +); + +server.registerTool( + 'getTask', + { + title: 'Get a task', + inputSchema: { id: z.string() }, + outputSchema: BackgroundAgentTaskSchema.shape, + }, + ({ id }) => { + const task = idToTask.get(id); + if (!task) { + return { + isError: true, + content: [ + { + type: 'text', + text: 'No such task', + }, + ], + }; + } + + return { + content: [], + structuredContent: task, + }; + }, +); + +server.registerTool( + 'listTasks', + { + title: 'Lists tasks', + outputSchema: { + tasks: z.array(BackgroundAgentTaskSchema), + }, + }, + () => { + const out = { + tasks: Array.from(idToTask.values()), + }; + return { + content: [], + structuredContent: out, + }; + }, +); + +server.registerTool( + 'messageTask', + { + title: 'Send a message to a task', + inputSchema: { + id: z.string(), + message: BackgroundAgentMessageSchema, + }, + }, + ({ id, message }) => { + const task = idToTask.get(id); + if (!task) { + return { + isError: true, + content: [ + { + type: 'text', + text: 'No such task', + }, + ], + }; + } + + task.history?.push(message); + task.status.message = message; + + const statuses = BackgroundAgentTaskStatusSchema.shape.state.options; + const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]; + task.status.state = randomStatus; + + return { + content: [], + }; + }, +); + +server.registerTool( + 'deleteTask', + { + title: 'Delete a task', + inputSchema: { id: z.string() }, + }, + ({ id }) => { + const task = idToTask.get(id); + if (!task) { + return { + isError: true, + content: [ + { + type: 'text', + text: 'No such task', + }, + ], + }; + } + idToTask.delete(id); + + return { + content: [ + { + type: 'text', + text: 'Task deleted', + }, + ], + }; + }, +); + +server.registerTool( + 'cancelTask', + { + title: 'Cancels a task', + inputSchema: { id: z.string() }, + }, + ({ id }) => { + const task = idToTask.get(id); + if (!task) { + return { + isError: true, + content: [ + { + type: 'text', + text: 'No such task', + }, + ], + }; + } + task.status.state = 'canceled'; + + return { + content: [ + { + type: 'text', + text: 'Task cancelled', + }, + ], + }; + }, +); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 00000000..133d1b75 --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "@google/gemini-cli-examples", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start:demo-background-agent": "tsx background_agent/demo-background-agent.ts", + "build": "echo 'nothing to build'" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.15.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.16.2" + } +}