Skip to content

Commit 2eacf15

Browse files
authored
Showing static tool metatada in tools docs. (#832)
1 parent 0cf4143 commit 2eacf15

14 files changed

Lines changed: 1223 additions & 145 deletions

app/_components/toolkit-docs/components/available-tools-table.tsx

Lines changed: 450 additions & 124 deletions
Large diffs are not rendered by default.

app/_components/toolkit-docs/components/dynamic-code-block.tsx

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -351,26 +351,33 @@ export function DynamicCodeBlock({
351351
return (
352352
<div className="my-4">
353353
{isExpanded ? (
354-
<div className="mt-3 space-y-3">
355-
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md bg-muted/20 p-3">
356-
<LanguageTabs
357-
currentLanguage={selectedLanguage}
358-
languages={availableLanguages}
359-
onSelect={setSelectedLanguage}
360-
/>
354+
<div className="mt-3 overflow-hidden rounded-lg bg-neutral-dark/10">
355+
<div className="flex flex-wrap items-center justify-between gap-3 bg-neutral-dark/20 px-4 py-2.5">
356+
<div className="flex items-center gap-3">
357+
<span className="text-sm font-medium text-foreground">
358+
Example
359+
</span>
360+
<div className="h-4 w-px bg-neutral-dark-high/30" />
361+
<LanguageTabs
362+
currentLanguage={selectedLanguage}
363+
languages={availableLanguages}
364+
onSelect={setSelectedLanguage}
365+
/>
366+
</div>
361367
<Button
362-
className="text-muted-foreground hover:text-foreground"
368+
aria-label="Hide example"
369+
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
363370
onClick={() => setIsExpanded(false)}
364-
size="sm"
371+
size="icon"
365372
variant="ghost"
366373
>
367-
Close
374+
<ChevronDown className="h-4 w-4 rotate-180 transition-transform" />
368375
</Button>
369376
</div>
370377

371-
<div className="relative rounded-md border border-muted/80">
372-
<div className="flex items-center justify-between border-muted border-b bg-muted/5 px-3 py-2">
373-
<span className="font-mono text-muted-foreground text-xs">
378+
<div className="relative">
379+
<div className="flex items-center justify-between border-b border-muted bg-muted/5 px-3 py-2">
380+
<span className="font-mono text-xs text-muted-foreground">
374381
{selectedLanguage.toLowerCase()}
375382
</span>
376383
<CopyButton
@@ -398,13 +405,18 @@ export function DynamicCodeBlock({
398405
</div>
399406
) : (
400407
<Button
401-
className="mt-2"
408+
aria-expanded={false}
409+
className="group mt-2 h-auto w-full justify-between rounded-lg bg-neutral-dark/10 px-4 py-3 text-left text-sm font-medium text-foreground hover:bg-neutral-dark/20"
402410
onClick={() => setIsExpanded(true)}
403-
size="sm"
404-
variant="outline"
411+
variant="ghost"
405412
>
406-
See example
407-
<ChevronDown className="ml-1 h-3 w-3" />
413+
<span className="flex flex-col items-start">
414+
<span>See example</span>
415+
<span className="text-muted-foreground/80 text-xs font-normal">
416+
Expand Python and TypeScript snippets
417+
</span>
418+
</span>
419+
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-hover:text-foreground" />
408420
</Button>
409421
)}
410422
</div>

app/_components/toolkit-docs/components/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export {
1111
export { DynamicCodeBlock } from "./dynamic-code-block";
1212
export { ParametersTable } from "./parameters-table";
1313
export { ScopesDisplay } from "./scopes-display";
14+
export {
15+
buildBehaviorRows,
16+
ToolMetadataSection,
17+
} from "./tool-metadata-section";
1418
export { ToolSection } from "./tool-section";
1519
export { ToolkitHeader } from "./toolkit-header";
1620
export { ToolkitPage } from "./toolkit-page";
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"use client";
2+
3+
import { Badge } from "@arcadeai/design-system";
4+
import { Check, ChevronDown, Lightbulb, Minus, X } from "lucide-react";
5+
import {
6+
TOOL_METADATA_FALLBACK_STYLE,
7+
TOOL_METADATA_OPERATION_STYLES,
8+
TOOL_METADATA_SERVICE_DOMAIN_STYLES,
9+
} from "../constants";
10+
import type { ToolMetadata, ToolMetadataBehavior } from "../types";
11+
12+
type BehaviorFlagKey = "readOnly" | "destructive" | "idempotent" | "openWorld";
13+
14+
const BEHAVIOR_LABELS: Record<BehaviorFlagKey, string> = {
15+
readOnly: "Read only",
16+
destructive: "Destructive",
17+
idempotent: "Idempotent",
18+
openWorld: "Open world",
19+
};
20+
21+
const BEHAVIOR_DESCRIPTIONS: Record<BehaviorFlagKey, string> = {
22+
readOnly: "Does not modify remote state.",
23+
destructive: "May delete or overwrite remote data.",
24+
idempotent: "Safe to retry without extra side effects.",
25+
openWorld: "Can call out to external systems.",
26+
};
27+
28+
export type BehaviorRow = {
29+
key: BehaviorFlagKey;
30+
label: string;
31+
value: boolean | null;
32+
};
33+
34+
export function buildBehaviorRows(
35+
behavior: ToolMetadataBehavior
36+
): readonly BehaviorRow[] {
37+
return (Object.keys(BEHAVIOR_LABELS) as BehaviorFlagKey[]).map((key) => ({
38+
key,
39+
label: BEHAVIOR_LABELS[key],
40+
value: behavior[key] ?? null,
41+
}));
42+
}
43+
44+
function formatEnumLabel(value: string): string {
45+
const words = value.split("_");
46+
return words
47+
.map((word, index) => {
48+
if (word === "crm") {
49+
return "CRM";
50+
}
51+
if (index === 0) {
52+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
53+
}
54+
return word.toLowerCase();
55+
})
56+
.join(" ");
57+
}
58+
59+
function EnumBadge({
60+
value,
61+
styles,
62+
}: {
63+
value: string;
64+
styles: Record<string, string>;
65+
}) {
66+
const styleClass = styles[value] ?? TOOL_METADATA_FALLBACK_STYLE;
67+
return (
68+
<Badge
69+
className={`${styleClass} gap-1.5 border-0 text-xs font-medium`}
70+
variant="secondary"
71+
>
72+
<span aria-hidden className="h-1.5 w-1.5 rounded-full bg-current/80" />
73+
{formatEnumLabel(value)}
74+
</Badge>
75+
);
76+
}
77+
78+
function BooleanBadge({ value }: { value: boolean | null }) {
79+
if (value === null) {
80+
return (
81+
<Badge
82+
className="border-0 bg-neutral-dark/40 text-muted-foreground/70"
83+
variant="secondary"
84+
>
85+
<Minus className="mr-1 h-3 w-3" /> Unknown
86+
</Badge>
87+
);
88+
}
89+
90+
return value ? (
91+
<Badge
92+
className="border-0 bg-emerald-500/15 text-emerald-300"
93+
variant="secondary"
94+
>
95+
<Check className="mr-1 h-3 w-3" /> Yes
96+
</Badge>
97+
) : (
98+
<Badge
99+
className="border-0 bg-neutral-500/15 text-neutral-300"
100+
variant="secondary"
101+
>
102+
<X className="mr-1 h-3 w-3" /> No
103+
</Badge>
104+
);
105+
}
106+
107+
export function ToolMetadataSection({
108+
metadata,
109+
}: {
110+
metadata?: ToolMetadata | null;
111+
}) {
112+
if (!metadata) {
113+
return null;
114+
}
115+
116+
const behaviorRows = buildBehaviorRows(metadata.behavior);
117+
const hasOperations = metadata.behavior.operations.length > 0;
118+
const hasServiceDomains = metadata.classification.serviceDomains.length > 0;
119+
const hasAnyBehaviorValue = behaviorRows.some((row) => row.value !== null);
120+
const hasExtras =
121+
metadata.extras != null && Object.keys(metadata.extras).length > 0;
122+
123+
if (
124+
!(hasOperations || hasServiceDomains || hasAnyBehaviorValue || hasExtras)
125+
) {
126+
return null;
127+
}
128+
129+
return (
130+
<div className="mb-6 rounded-xl bg-neutral-dark/15 p-4">
131+
<div className="mb-4 flex items-start gap-2.5">
132+
<div className="mt-0.5 rounded-md bg-brand-accent/10 p-1.5 text-brand-accent">
133+
<Lightbulb className="h-3.5 w-3.5" />
134+
</div>
135+
<div>
136+
<h4 className="font-semibold text-foreground text-sm">
137+
Execution hints
138+
</h4>
139+
<p className="mt-1 text-muted-foreground/75 text-xs">
140+
Signals for MCP clients and agents about how this tool behaves.
141+
</p>
142+
</div>
143+
</div>
144+
145+
<div className="space-y-3">
146+
{(hasOperations || hasServiceDomains) && (
147+
<div className="grid gap-3 md:grid-cols-2">
148+
{hasOperations && (
149+
<div className="rounded-lg bg-neutral-dark/20 p-3">
150+
<span className="mb-2 block text-muted-foreground/80 text-xs font-medium">
151+
Operations
152+
</span>
153+
<div className="flex flex-wrap items-center gap-2">
154+
{metadata.behavior.operations.map((operation) => (
155+
<EnumBadge
156+
key={operation}
157+
styles={TOOL_METADATA_OPERATION_STYLES}
158+
value={operation}
159+
/>
160+
))}
161+
</div>
162+
</div>
163+
)}
164+
165+
{hasServiceDomains && (
166+
<div className="rounded-lg bg-neutral-dark/20 p-3">
167+
<span className="mb-2 block text-muted-foreground/80 text-xs font-medium">
168+
Service domains
169+
</span>
170+
<div className="flex flex-wrap items-center gap-2">
171+
{metadata.classification.serviceDomains.map((domain) => (
172+
<EnumBadge
173+
key={domain}
174+
styles={TOOL_METADATA_SERVICE_DOMAIN_STYLES}
175+
value={domain}
176+
/>
177+
))}
178+
</div>
179+
</div>
180+
)}
181+
</div>
182+
)}
183+
184+
{hasAnyBehaviorValue && (
185+
<div className="rounded-lg bg-neutral-dark/20 p-3">
186+
<span className="mb-2 block text-muted-foreground/80 text-xs font-medium">
187+
MCP behavior
188+
</span>
189+
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
190+
{behaviorRows.map((row) => (
191+
<div
192+
className="rounded-md bg-neutral-dark/25 p-2.5"
193+
key={row.key}
194+
>
195+
<div className="flex items-center justify-between gap-2">
196+
<span className="font-medium text-foreground text-xs">
197+
{row.label}
198+
</span>
199+
<div className="flex shrink-0">
200+
<BooleanBadge value={row.value} />
201+
</div>
202+
</div>
203+
<p className="mt-1.5 text-[11px] text-muted-foreground/70 leading-relaxed">
204+
{BEHAVIOR_DESCRIPTIONS[row.key]}
205+
</p>
206+
</div>
207+
))}
208+
</div>
209+
</div>
210+
)}
211+
212+
{hasExtras && (
213+
<details className="group mt-2 rounded-lg bg-neutral-dark/20">
214+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-2.5 text-muted-foreground/85 text-xs font-medium">
215+
Additional metadata
216+
<ChevronDown className="h-3.5 w-3.5 transition-transform group-open:rotate-180" />
217+
</summary>
218+
<pre className="overflow-auto p-3 text-muted-foreground text-xs">
219+
{JSON.stringify(metadata.extras, null, 2)}
220+
</pre>
221+
</details>
222+
)}
223+
</div>
224+
</div>
225+
);
226+
}

app/_components/toolkit-docs/components/tool-section.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { DynamicCodeBlock } from "./dynamic-code-block";
1313
import { ParametersTable } from "./parameters-table";
1414
import { ScopesDisplay } from "./scopes-display";
15+
import { ToolMetadataSection } from "./tool-metadata-section";
1516

1617
const COPY_FEEDBACK_MS = 2000;
1718
const JSON_PRETTY_PRINT_INDENT = 2;
@@ -484,6 +485,7 @@ export function ToolSection({
484485
showSelection={showSelection}
485486
tool={tool}
486487
/>
488+
<ToolMetadataSection metadata={tool.metadata} />
487489
<ToolDescriptionSection showDescription={showDescription} tool={tool} />
488490
<ToolParametersSection showParameters={showParameters} tool={tool} />
489491
<ToolRequirementsSection

0 commit comments

Comments
 (0)