Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d38aef8
docs: add typegen CLI design doc
viktormarinho Feb 25, 2026
95ab1f5
docs: add typegen implementation plan
viktormarinho Feb 25, 2026
e05cfbd
feat(typegen): scaffold package structure
viktormarinho Feb 25, 2026
610e65a
feat(typegen): add public types
viktormarinho Feb 25, 2026
5c4caaf
feat(typegen): add createMeshClient runtime with Proxy
viktormarinho Feb 25, 2026
5a8e632
feat(typegen): add codegen — schema to TypeScript client
viktormarinho Feb 25, 2026
b0ec0b1
feat(typegen): add CLI entry point
viktormarinho Feb 25, 2026
22d77e4
feat(typegen): build verification and formatting
viktormarinho Feb 25, 2026
d6190f6
docs(typegen): add README
viktormarinho Feb 25, 2026
16a04aa
feat(typegen): add Generate typed client section to Share Agent dialog
viktormarinho Feb 25, 2026
16aefad
fix(typegen): widen Share Agent dialog to max-w-3xl
viktormarinho Feb 25, 2026
8afaed0
fix(typegen): prevent long command string from blowing out dialog width
viktormarinho Feb 25, 2026
7459eec
fix(typegen): use sm:max-w-2xl to override dialog default sm:max-w-lg
viktormarinho Feb 25, 2026
729b4a1
fix(typegen): add min-w-0 to typegen section root to contain long key…
viktormarinho Feb 25, 2026
6a1753f
fix(typegen): fix unclosed div and use break-all to contain long API key
viktormarinho Feb 25, 2026
93b29a7
feat(typegen): add env vars code block to Share Agent dialog
viktormarinho Feb 25, 2026
46fdc54
feat(typegen): add section titles and --output client.ts to command
viktormarinho Feb 25, 2026
8078e18
feat(typegen): rename Share to Connect button with Link01 icon
viktormarinho Feb 25, 2026
c377e80
feat(typegen): swap Connect button icon to ZapCircle
viktormarinho Feb 25, 2026
7384ca9
fix(typegen): fix race condition, URL construction, injection risk in…
viktormarinho Feb 25, 2026
c5a9bf7
ci(typegen): add npm publish workflow
viktormarinho Feb 25, 2026
6cd8e01
fix(typegen): expose close() on MeshClient to prevent connection leaks
viktormarinho Feb 25, 2026
8e8531d
fix(typegen): add knip ignore for test files
viktormarinho Feb 25, 2026
aeb43e1
fix(typegen): restore module mocks in afterAll to prevent leak into o…
viktormarinho Feb 25, 2026
d2e1369
fix(typegen): replace mock.module with dependency injection to preven…
viktormarinho Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/publish-typegen-npm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Publish @decocms/typegen

on:
push:
branches: [main]
paths:
- "packages/typegen/**"
workflow_dispatch:

permissions:
contents: write
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Setup Node.js for npm registry
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: bun install

- name: Build
run: bun run build
working-directory: packages/typegen

- name: Check if version changed
id: version-check
run: |
CURRENT_VERSION=$(node -e "console.log(require('./package.json').version)")

if npm view @decocms/typegen@$CURRENT_VERSION version >/dev/null 2>&1; then
echo "version-changed=false" >> $GITHUB_OUTPUT
echo "⏭️ Version $CURRENT_VERSION already published, skipping publish"
else
echo "version-changed=true" >> $GITHUB_OUTPUT
echo "✅ Version $CURRENT_VERSION not found in npm, will publish"
fi

echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT

if [[ "$CURRENT_VERSION" == *-* ]]; then
echo "npm-tag=next" >> $GITHUB_OUTPUT
echo "📦 Prerelease version detected, will publish with tag 'next'"
else
echo "npm-tag=latest" >> $GITHUB_OUTPUT
echo "📦 Stable version detected, will publish with tag 'latest'"
fi
working-directory: packages/typegen

- name: Publish to npm
if: steps.version-check.outputs.version-changed == 'true'
run: npm publish --access public --tag ${{ steps.version-check.outputs.npm-tag }} --provenance
working-directory: packages/typegen

- name: Create Release
if: steps.version-check.outputs.version-changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "typegen-v${{ steps.version-check.outputs.current-version }}" \
--title "@decocms/typegen v${{ steps.version-check.outputs.current-version }}" \
--notes "## Changes

Automated release of @decocms/typegen package.

### Installation
\`\`\`bash
npm install @decocms/typegen
\`\`\`

### Usage
\`\`\`bash
bunx @decocms/typegen --mcp <virtual-mcp-id> --key <api-key> --output client.ts
\`\`\`"
31 changes: 12 additions & 19 deletions apps/mesh/src/web/components/details/virtual-mcp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
Loading01,
Play,
Plus,
Share07,
ZapCircle,
Tool01,
Users03,
} from "@untitledui/icons";
Expand Down Expand Up @@ -308,24 +308,17 @@ function VirtualMcpDetailViewWithData({
<TooltipContent side="bottom">Test this agent in chat</TooltipContent>
</Tooltip>

<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
variant="outline"
size="icon"
className="size-7 border border-input"
onClick={() =>
dispatch({ type: "SET_SHARE_DIALOG_OPEN", payload: true })
}
aria-label="Share"
>
<Share07 size={14} />
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">Share</TooltipContent>
</Tooltip>
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5 px-2 border border-input"
onClick={() =>
dispatch({ type: "SET_SHARE_DIALOG_OPEN", payload: true })
}
>
<ZapCircle size={14} />
Connect
</Button>

<PinToSidebarButton
title={virtualMcp.title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@deco/ui/components/tooltip.tsx";
import {
SELF_MCP_ALIAS_ID,
useMCPClient,
useProjectContext,
} from "@decocms/mesh-sdk";
import type { VirtualMCPEntity } from "@decocms/mesh-sdk";
import {
ArrowsRight,
Check,
Code01,
Copy01,
InfoCircle,
Key01,
Lightbulb02,
Loading01,
} from "@untitledui/icons";
import { useState } from "react";
import { cn } from "@deco/ui/lib/utils.ts";
import { Suspense, useState } from "react";
import { toast } from "sonner";

/**
Expand Down Expand Up @@ -178,6 +186,171 @@ function InstallClaudeButton({ url, serverName }: ShareWithNameProps) {
);
}

/**
* Typegen section inner — uses Suspense-based useMCPClient
*/
function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) {
const { org } = useProjectContext();
const client = useMCPClient({
connectionId: SELF_MCP_ALIAS_ID,
orgId: org.id,
});
const [apiKey, setApiKey] = useState<string | null>(null);
const [generating, setGenerating] = useState(false);
const [copied, setCopied] = useState(false);

const mcpId = virtualMcp.id;
const agentName = virtualMcp.title || `agent-${mcpId.slice(0, 8)}`;
const command = apiKey
? `bunx @decocms/typegen@latest --mcp ${mcpId} --key ${apiKey} --output client.ts`
: `bunx @decocms/typegen@latest --mcp ${mcpId} --key <api-key> --output client.ts`;

const handleGenerateKey = async () => {
setGenerating(true);
try {
const result = (await client.callTool({
name: "API_KEY_CREATE",
arguments: {
name: `typegen-${agentName}`,
permissions: { [mcpId]: ["*"] },
},
})) as { structuredContent?: { key?: string } };
const key = result.structuredContent?.key;
if (!key) throw new Error("No key in response");
setApiKey(key);
} catch {
toast.error("Failed to generate API key");
} finally {
setGenerating(false);
}
};

const handleCopy = async () => {
await navigator.clipboard.writeText(command);
setCopied(true);
toast.success("Command copied to clipboard");
setTimeout(() => setCopied(false), 2000);
};

return (
<div className="flex min-w-0 flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-0.5">
<h4 className="text-sm font-medium text-foreground">
Generate typed client
</h4>
<p className="text-xs text-muted-foreground">
Introspects this agent and writes a typed{" "}
<code className="font-mono">client.ts</code> you can import
directly.
</p>
</div>
{!apiKey && (
<Button
type="button"
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
onClick={handleGenerateKey}
disabled={generating}
>
{generating ? (
<Loading01 size={14} className="animate-spin" />
) : (
<Key01 size={14} />
)}
<span>{generating ? "Generating…" : "Generate API key"}</span>
</Button>
)}
</div>

{apiKey && (
<p className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
Store this key securely — it won't be shown again.
</p>
)}

<p className="text-xs font-medium text-muted-foreground">
Generate client
</p>
<div className="rounded-md border border-input bg-muted/50 px-3 py-2.5">
<div className="flex items-start gap-2">
<code className="min-w-0 flex-1 break-all font-mono text-xs text-muted-foreground">
{command}
</code>
<Button
type="button"
variant="ghost"
size="icon"
className="size-6 shrink-0"
onClick={handleCopy}
>
{copied ? (
<Check size={12} className="text-green-600" />
) : (
<Copy01 size={12} />
)}
</Button>
</div>
</div>

<p className="text-xs font-medium text-muted-foreground">
Runtime variables
</p>
<EnvVarsBlock apiKey={apiKey} />
</div>
);
}

function EnvVarsBlock({ apiKey }: { apiKey: string | null }) {
const [copied, setCopied] = useState(false);
const meshUrl = window.location.origin;
const keyLine = apiKey ? `MESH_API_KEY=${apiKey}` : `MESH_API_KEY=<api-key>`;
const urlLine = `MESH_BASE_URL=${meshUrl}`;
const envBlock = `${keyLine}\n${urlLine}`;

const handleCopy = async () => {
await navigator.clipboard.writeText(envBlock);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className="rounded-md border border-input bg-muted/50 px-3 py-2.5">
<div className="flex items-start gap-2">
<code className="min-w-0 flex-1 font-mono text-xs text-muted-foreground">
<span className={cn({ "opacity-50": !apiKey })}>{keyLine}</span>
<br />
<span>{urlLine}</span>
</code>
<Button
type="button"
variant="ghost"
size="icon"
className="size-6 shrink-0"
onClick={handleCopy}
>
{copied ? (
<Check size={12} className="text-green-600" />
) : (
<Copy01 size={12} />
)}
</Button>
</div>
</div>
);
}

function TypegenSection({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) {
return (
<Suspense
fallback={<div className="h-20 animate-pulse rounded-md bg-muted" />}
>
<TypegenSectionInner virtualMcp={virtualMcp} />
</Suspense>
);
}

/**
* Share Modal - Virtual MCP sharing and IDE integration
*/
Expand Down Expand Up @@ -218,9 +391,9 @@ export function VirtualMCPShareModal({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogContent className="max-h-[80vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Share Agent</DialogTitle>
<DialogTitle>Connect</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-6">
{/* Mode Selection */}
Expand Down Expand Up @@ -363,6 +536,11 @@ export function VirtualMCPShareModal({
/>
</div>
</div>

<div className="border-t border-border" />

{/* Typegen */}
<TypegenSection virtualMcp={virtualMcp} />
</div>
</DialogContent>
</Dialog>
Expand Down
Loading