Skip to content

Commit 6e665b5

Browse files
committed
Merge branch 'main' into parse-tool-arg
2 parents 6a2dcb0 + 7f1d924 commit 6e665b5

File tree

11 files changed

+463
-247
lines changed

11 files changed

+463
-247
lines changed

cli/src/client/connection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export async function connect(
1818
): Promise<void> {
1919
try {
2020
await client.connect(transport);
21+
22+
if (client.getServerCapabilities()?.logging) {
23+
// default logging level is undefined in the spec, but the user of the
24+
// inspector most likely wants debug.
25+
await client.setLoggingLevel("debug");
26+
}
2127
} catch (error) {
2228
throw new Error(
2329
`Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,

client/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,19 @@
4949
"serve-handler": "^6.1.6",
5050
"tailwind-merge": "^2.5.3",
5151
"tailwindcss-animate": "^1.0.7",
52-
"zod": "^3.23.8"
52+
"zod": "^3.25.76"
5353
},
5454
"devDependencies": {
5555
"@eslint/js": "^9.11.1",
5656
"@testing-library/jest-dom": "^6.6.3",
5757
"@testing-library/react": "^16.2.0",
5858
"@types/jest": "^29.5.14",
59-
"@types/node": "^22.7.5",
59+
"@types/node": "^22.17.0",
6060
"@types/prismjs": "^1.26.5",
61-
"@types/react": "^18.3.10",
61+
"@types/react": "^18.3.23",
6262
"@types/react-dom": "^18.3.0",
6363
"@types/serve-handler": "^6.1.4",
64-
"@vitejs/plugin-react": "^4.3.2",
64+
"@vitejs/plugin-react": "^4.7.0",
6565
"autoprefixer": "^10.4.20",
6666
"co": "^4.6.0",
6767
"eslint": "^9.11.1",
@@ -70,11 +70,11 @@
7070
"globals": "^15.9.0",
7171
"jest": "^29.7.0",
7272
"jest-environment-jsdom": "^29.7.0",
73-
"postcss": "^8.4.47",
73+
"postcss": "^8.5.6",
7474
"tailwindcss": "^3.4.13",
75-
"ts-jest": "^29.2.6",
75+
"ts-jest": "^29.4.0",
7676
"typescript": "^5.5.3",
77-
"typescript-eslint": "^8.7.0",
78-
"vite": "^6.3.0"
77+
"typescript-eslint": "^8.38.0",
78+
"vite": "^6.3.5"
7979
}
8080
}

client/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ const App = () => {
260260
window.location.hash = "elicitations";
261261
},
262262
getRoots: () => rootsRef.current,
263+
defaultLoggingLevel: logLevel,
263264
});
264265

265266
useEffect(() => {

client/src/components/ToolResults.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ const ToolResults = ({
154154
</div>
155155
</div>
156156
)}
157+
{structuredResult._meta && (
158+
<div className="mb-4">
159+
<h5 className="font-semibold mb-2 text-sm">Meta:</h5>
160+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
161+
<JsonView data={structuredResult._meta} />
162+
</div>
163+
</div>
164+
)}
157165
{!structuredResult.structuredContent &&
158166
validationResult &&
159167
!validationResult.isValid && (

client/src/components/ToolsTab.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import ListPane from "./ListPane";
1919
import JsonView from "./JsonView";
2020
import ToolResults from "./ToolResults";
2121

22+
// Type guard to safely detect the optional _meta field without using `any`
23+
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
24+
typeof (tool as { _meta?: unknown })._meta !== "undefined";
25+
2226
const ToolsTab = ({
2327
tools,
2428
listTools,
@@ -46,6 +50,7 @@ const ToolsTab = ({
4650
const [params, setParams] = useState<Record<string, unknown>>({});
4751
const [isToolRunning, setIsToolRunning] = useState(false);
4852
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
53+
const [isMetaExpanded, setIsMetaExpanded] = useState(false);
4954

5055
useEffect(() => {
5156
const params = Object.entries(
@@ -245,6 +250,40 @@ const ToolsTab = ({
245250
</div>
246251
</div>
247252
)}
253+
{selectedTool &&
254+
hasMeta(selectedTool) &&
255+
selectedTool._meta && (
256+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
257+
<div className="flex items-center justify-between mb-2">
258+
<h4 className="text-sm font-semibold">Meta:</h4>
259+
<Button
260+
size="sm"
261+
variant="ghost"
262+
onClick={() => setIsMetaExpanded(!isMetaExpanded)}
263+
className="h-6 px-2"
264+
>
265+
{isMetaExpanded ? (
266+
<>
267+
<ChevronUp className="h-3 w-3 mr-1" />
268+
Collapse
269+
</>
270+
) : (
271+
<>
272+
<ChevronDown className="h-3 w-3 mr-1" />
273+
Expand
274+
</>
275+
)}
276+
</Button>
277+
</div>
278+
<div
279+
className={`transition-all ${
280+
isMetaExpanded ? "" : "max-h-[8rem] overflow-y-auto"
281+
}`}
282+
>
283+
<JsonView data={selectedTool._meta} />
284+
</div>
285+
</div>
286+
)}
248287
<Button
249288
onClick={async () => {
250289
try {

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ToolsTab from "../ToolsTab";
55
import { Tool } from "@modelcontextprotocol/sdk/types.js";
66
import { Tabs } from "@/components/ui/tabs";
77
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
8+
import { within } from "@testing-library/react";
89

910
describe("ToolsTab", () => {
1011
beforeEach(() => {
@@ -556,4 +557,84 @@ describe("ToolsTab", () => {
556557
expect(mockOnReadResource).toHaveBeenCalledTimes(1);
557558
});
558559
});
560+
561+
describe("Meta Display", () => {
562+
const toolWithMeta = {
563+
name: "metaTool",
564+
description: "Tool with meta",
565+
inputSchema: {
566+
type: "object" as const,
567+
properties: {
568+
foo: { type: "string" as const },
569+
},
570+
},
571+
_meta: {
572+
author: "tester",
573+
version: 1,
574+
},
575+
} as unknown as Tool;
576+
577+
it("should display meta section when tool has _meta", () => {
578+
renderToolsTab({
579+
tools: [toolWithMeta],
580+
selectedTool: toolWithMeta,
581+
});
582+
583+
expect(screen.getByText("Meta:")).toBeInTheDocument();
584+
expect(
585+
screen.getByRole("button", { name: /expand/i }),
586+
).toBeInTheDocument();
587+
});
588+
589+
it("should toggle meta expansion", () => {
590+
renderToolsTab({
591+
tools: [toolWithMeta],
592+
selectedTool: toolWithMeta,
593+
});
594+
595+
// There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section
596+
const metaHeading = screen.getByText("Meta:");
597+
const metaContainer = metaHeading.closest("div");
598+
expect(metaContainer).toBeTruthy();
599+
const toggleButton = within(metaContainer as HTMLElement).getByRole(
600+
"button",
601+
{ name: /expand/i },
602+
);
603+
604+
// Expand Meta
605+
fireEvent.click(toggleButton);
606+
expect(
607+
within(metaContainer as HTMLElement).getByRole("button", {
608+
name: /collapse/i,
609+
}),
610+
).toBeInTheDocument();
611+
612+
// Collapse Meta
613+
fireEvent.click(toggleButton);
614+
expect(
615+
within(metaContainer as HTMLElement).getByRole("button", {
616+
name: /expand/i,
617+
}),
618+
).toBeInTheDocument();
619+
});
620+
});
621+
622+
describe("ToolResults Meta", () => {
623+
it("should display meta information when present in toolResult", () => {
624+
const resultWithMeta = {
625+
content: [],
626+
_meta: { info: "details", version: 2 },
627+
};
628+
629+
renderToolsTab({
630+
selectedTool: mockTools[0],
631+
toolResult: resultWithMeta,
632+
});
633+
634+
// Only ToolResults meta should be present since selectedTool has no _meta
635+
expect(screen.getAllByText("Meta:")).toHaveLength(1);
636+
expect(screen.getByText(/info/i)).toBeInTheDocument();
637+
expect(screen.getByText(/version/i)).toBeInTheDocument();
638+
});
639+
});
559640
});

client/src/lib/hooks/useConnection.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ToolListChangedNotificationSchema,
2929
PromptListChangedNotificationSchema,
3030
Progress,
31+
LoggingLevel,
3132
ElicitRequestSchema,
3233
} from "@modelcontextprotocol/sdk/types.js";
3334
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
@@ -72,6 +73,7 @@ interface UseConnectionOptions {
7273
onElicitationRequest?: (request: any, resolve: any) => void;
7374
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7475
getRoots?: () => any[];
76+
defaultLoggingLevel?: LoggingLevel;
7577
}
7678

7779
export function useConnection({
@@ -90,6 +92,7 @@ export function useConnection({
9092
onPendingRequest,
9193
onElicitationRequest,
9294
getRoots,
95+
defaultLoggingLevel,
9396
}: UseConnectionOptions) {
9497
const [connectionStatus, setConnectionStatus] =
9598
useState<ConnectionStatus>("disconnected");
@@ -560,6 +563,10 @@ export function useConnection({
560563
});
561564
}
562565

566+
if (capabilities?.logging && defaultLoggingLevel) {
567+
await client.setLoggingLevel(defaultLoggingLevel);
568+
}
569+
563570
if (onElicitationRequest) {
564571
client.setRequestHandler(ElicitRequestSchema, async (request) => {
565572
return new Promise((resolve) => {

client/src/lib/oauth-state-machine.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,21 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
113113
scope = metadata.scopes_supported.join(" ");
114114
}
115115

116+
// Generate a random state
117+
const array = new Uint8Array(32);
118+
crypto.getRandomValues(array);
119+
const state = Array.from(array, (byte) =>
120+
byte.toString(16).padStart(2, "0"),
121+
).join("");
122+
116123
const { authorizationUrl, codeVerifier } = await startAuthorization(
117124
context.serverUrl,
118125
{
119126
metadata,
120127
clientInformation,
121128
redirectUrl: context.provider.redirectUrl,
122129
scope,
130+
state: state,
123131
resource: context.state.resource ?? undefined,
124132
},
125133
);

0 commit comments

Comments
 (0)