From 51ccfe5b7d0f58c32c0209868a17e13ef6eaff05 Mon Sep 17 00:00:00 2001 From: Abhijay Jain Date: Wed, 24 Jun 2026 01:39:06 +0530 Subject: [PATCH] feat: add streamable HTTP support to deeplink generator (#9954) Signed-off-by: Abhijay Jain Co-authored-by: goose --- .../src/pages/deeplink-generator.tsx | 235 +++++++++++++++--- 1 file changed, 199 insertions(+), 36 deletions(-) diff --git a/documentation/src/pages/deeplink-generator.tsx b/documentation/src/pages/deeplink-generator.tsx index 29bb60973..e1ab46beb 100644 --- a/documentation/src/pages/deeplink-generator.tsx +++ b/documentation/src/pages/deeplink-generator.tsx @@ -2,7 +2,6 @@ import React, { useState, useCallback, useEffect } from 'react'; import Layout from "@theme/Layout"; import { Copy, Check, Plus, X } from "lucide-react"; import { Button } from "@site/src/components/ui/button"; -import Link from "@docusaurus/Link"; interface EnvironmentVariable { name: string; @@ -10,6 +9,14 @@ interface EnvironmentVariable { required: boolean; } +interface RequestHeader { + name: string; + description: string; + required: boolean; +} + +type ExtensionType = 'stdio' | 'streamable_http'; + interface ServerConfig { is_builtin: boolean; id: string; @@ -17,30 +24,34 @@ interface ServerConfig { description?: string; command?: string; url?: string; + type?: ExtensionType; environmentVariables: EnvironmentVariable[]; + headers?: RequestHeader[]; } export default function DeeplinkGenerator() { - // State management const [activeTab, setActiveTab] = useState<'form' | 'json'>('form'); const [isBuiltin, setIsBuiltin] = useState(false); + const [extensionType, setExtensionType] = useState('stdio'); const [id, setId] = useState(''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [command, setCommand] = useState(''); + const [endpointUrl, setEndpointUrl] = useState(''); const [envVars, setEnvVars] = useState([]); + const [headers, setHeaders] = useState([]); const [generatedLink, setGeneratedLink] = useState(''); const [jsonInput, setJsonInput] = useState(''); const [copied, setCopied] = useState(false); const [error, setError] = useState(''); - // Initialize JSON input with sample data useEffect(() => { const sampleJson = { is_builtin: false, + type: "stdio", id: "example-extension", name: "Example Extension", - description: "An example Goose extension", + description: "An example goose extension", command: "npx @gooseai/example-extension", environmentVariables: [ { @@ -53,12 +64,10 @@ export default function DeeplinkGenerator() { setJsonInput(JSON.stringify(sampleJson, null, 2)); }, []); - // Process URL parameters if present useEffect(() => { const urlParams = new URLSearchParams(window.location.search); if (urlParams.toString()) { try { - // Check if this is a built-in extension request if (urlParams.get('cmd') === 'goosed' && urlParams.getAll('arg').includes('mcp')) { const args = urlParams.getAll('arg'); const extensionId = args[args.indexOf('mcp') + 1]; @@ -66,7 +75,7 @@ export default function DeeplinkGenerator() { throw new Error('Missing extension ID in args'); } - const server = { + const server: ServerConfig = { is_builtin: true, id: extensionId, environmentVariables: [] @@ -76,7 +85,49 @@ export default function DeeplinkGenerator() { return; } - // Handle custom extension + const remoteUrl = urlParams.get('url'); + if (remoteUrl) { + const id = urlParams.get('id'); + const name = urlParams.get('name'); + const description = urlParams.get('description'); + if (!id || !name || !description) { + throw new Error('Missing required parameters. Need: id, name, and description'); + } + const server: ServerConfig = { + is_builtin: false, + type: 'streamable_http', + id, + name, + description, + url: remoteUrl, + environmentVariables: [], + headers: [], + }; + urlParams.getAll('env').forEach(env => { + const separatorIndex = env.indexOf('='); + if (separatorIndex > 0) { + server.environmentVariables.push({ + name: env.slice(0, separatorIndex), + description: env.slice(separatorIndex + 1), + required: true, + }); + } + }); + urlParams.getAll('header').forEach(header => { + const separatorIndex = header.indexOf('='); + if (separatorIndex > 0) { + server.headers.push({ + name: header.slice(0, separatorIndex), + description: header.slice(separatorIndex + 1), + required: true, + }); + } + }); + const link = generateDeeplink(server); + handleGeneratedLink(link, true); + return; + } + const cmd = urlParams.get('cmd'); if (!cmd) { throw new Error('Missing required parameter: cmd'); @@ -92,8 +143,9 @@ export default function DeeplinkGenerator() { throw new Error('Missing required parameters. Need: id, name, and description'); } - const server = { + const server: ServerConfig = { is_builtin: false, + type: 'stdio', id, name, description, @@ -101,15 +153,14 @@ export default function DeeplinkGenerator() { environmentVariables: [] }; - // Handle environment variables if present const envVars = urlParams.getAll('env'); if (envVars.length > 0) { envVars.forEach(env => { - const [name, description] = env.split('='); - if (name && description) { + const separatorIndex = env.indexOf('='); + if (separatorIndex > 0) { server.environmentVariables.push({ - name, - description, + name: env.slice(0, separatorIndex), + description: env.slice(separatorIndex + 1), required: true }); } @@ -144,18 +195,23 @@ export default function DeeplinkGenerator() { return `goose://extension?${queryParams}`; } - // Handle the case where the command is a URL - if (server.url) { + if (server.url || server.type === 'streamable_http') { const queryParams = [ - `url=${encodeURIComponent(server.url)}`, + `type=streamable_http`, + `url=${encodeURIComponent(server.url || '')}`, `id=${encodeURIComponent(server.id)}`, `name=${encodeURIComponent(server.name)}`, `description=${encodeURIComponent(server.description)}`, - ...server.environmentVariables + ...(server.environmentVariables || []) .filter((env) => env.required) .map( (env) => `env=${encodeURIComponent(`${env.name}=${env.description}`)}` ), + ...(server.headers || []) + .filter((header) => header.required) + .map( + (header) => `header=${encodeURIComponent(`${header.name}=${header.description}`)}` + ), ].join("&"); return `goose://extension?${queryParams}`; @@ -170,7 +226,7 @@ export default function DeeplinkGenerator() { `id=${encodeURIComponent(server.id)}`, `name=${encodeURIComponent(server.name)}`, `description=${encodeURIComponent(server.description)}`, - ...server.environmentVariables + ...(server.environmentVariables || []) .filter((env) => env.required) .map( (env) => `env=${encodeURIComponent(`${env.name}=${env.description}`)}` @@ -184,11 +240,14 @@ export default function DeeplinkGenerator() { e.preventDefault(); const server: ServerConfig = { is_builtin: isBuiltin, + type: extensionType, id, name, description, - command, - environmentVariables: envVars + command: extensionType === 'stdio' ? command : undefined, + url: extensionType === 'streamable_http' ? endpointUrl : undefined, + environmentVariables: envVars, + headers: extensionType === 'streamable_http' ? headers : undefined, }; try { @@ -197,7 +256,7 @@ export default function DeeplinkGenerator() { } catch (error) { setError(error.message); } - }, [isBuiltin, id, name, description, command, envVars]); + }, [isBuiltin, extensionType, id, name, description, command, endpointUrl, envVars, headers]); const handleJsonSubmit = useCallback(() => { try { @@ -218,11 +277,25 @@ export default function DeeplinkGenerator() { }, []); const handleEnvVarChange = useCallback((index: number, field: 'name' | 'description', value: string) => { - setEnvVars(prev => prev.map((env, i) => + setEnvVars(prev => prev.map((env, i) => i === index ? { ...env, [field]: value } : env )); }, []); + const handleAddHeader = useCallback(() => { + setHeaders(prev => [...prev, { name: '', description: '', required: true }]); + }, []); + + const handleRemoveHeader = useCallback((index: number) => { + setHeaders(prev => prev.filter((_, i) => i !== index)); + }, []); + + const handleHeaderChange = useCallback((index: number, field: 'name' | 'description', value: string) => { + setHeaders(prev => prev.map((h, i) => + i === index ? { ...h, [field]: value } : h + )); + }, []); + const handleCopy = useCallback(() => { navigator.clipboard.writeText(generatedLink) .then(() => { @@ -240,7 +313,7 @@ export default function DeeplinkGenerator() { Deeplink Generator

- Generate installation deeplinks for Goose extensions that can be shared with others. + Generate installation deeplinks for goose extensions that can be shared with others.

@@ -336,18 +409,64 @@ export default function DeeplinkGenerator() {
- setCommand(e.target.value)} - required - className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard" - placeholder="npx @gooseai/example-extension" - /> +
+ + +
+ {extensionType === 'stdio' ? ( +
+ + setCommand(e.target.value)} + required + className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard" + placeholder="npx @gooseai/example-extension" + /> +
+ ) : ( +
+ + setEndpointUrl(e.target.value)} + required + className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard" + placeholder="https://example.com/mcp" + /> +
+ )} +
+ + {extensionType === 'streamable_http' && ( +
+ +
+ {headers.map((header, index) => ( +
+ handleHeaderChange(index, 'name', e.target.value)} + className="flex-1 p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard" + placeholder="Header Name" + /> + handleHeaderChange(index, 'description', e.target.value)} + className="flex-1 p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard" + placeholder="Description" + /> + +
+ ))} + +
+
+ )} )} @@ -456,15 +618,16 @@ export default function DeeplinkGenerator() {
  • For custom extensions:
    • Provide a unique ID, name, and description
    • -
    • Enter the command used to run your extension
    • +
    • Choose STDIO and enter the command to run your extension (e.g. npx @gooseai/my-ext), or choose Streamable HTTP and enter the endpoint URL
    • Add any required environment variables
    • +
    • For Streamable HTTP, add any required request headers
  • Click "Generate Deeplink" to create your installation deeplink.
  • -
  • Copy and share the generated deeplink - when users click it, it will open Goose Desktop and prompt them to install your extension.
  • +
  • Copy and share the generated deeplink — when users click it, it will open goose Desktop and prompt them to install your extension.
  • ); -} \ No newline at end of file +}