Compare commits

..

13 Commits
0.5.1 ... 0.6.0

Author SHA1 Message Date
Justin Spahr-Summers
0281e5f821 Fix formatting 2025-03-11 10:56:53 +00:00
Justin Spahr-Summers
f56961ac62 Bump version 2025-03-11 10:55:14 +00:00
Justin Spahr-Summers
15bbb7502b Merge pull request #175 from avi1mizrahi/main
Add Bearer Token Support
2025-03-11 10:50:51 +00:00
Ola Hungerford
7caf6f8ba8 Merge pull request #170 from cliffhall/add-subscribe-to-resource
Add subscribe to resource functionality
2025-03-10 21:08:46 -07:00
Avi Mizrahi
dbd616905c Support bearer token 2025-03-10 17:50:12 +02:00
Cliff Hall
35a0f4611a Merge branch 'main' into add-subscribe-to-resource 2025-03-08 15:35:54 -05:00
cliffhall
952bee2605 Fix prettier complaints 2025-03-08 15:08:12 -05:00
cliffhall
a669272fda Track subscribed resources and show the appropriate subscribe or unsubscribe button on selected resource panel.
If the server does not support resource subscriptions, do not show any subscription buttons.

* In App.tsx
  - useState for resourceSubscriptions, setResourceSubscriptions a Set of type string.
  - in subscribeToResource()
    - only make the request to subscribe if the uri is not in the resourceSubscriptions set
  - in unsubscribeFromResource()
    - only make the request to unsubscribe if the uri is in the resourceSubscriptions set
  - in ResourceTab element,
    - pass a boolean resourceSubscriptionsSupported as serverCapabilities.resources.subscribe
    - pass resourceSubscriptions as a prop
* In ResourcesTab.tsx
  - deconstruct resourceSubscriptions and resourceSubscriptionsSupported from props and add prop type
  - in selected resource panel
    - don't show subscribe or unsubscribe buttons unless resourceSubscriptionsSupported is true
    - only show subscribe button if selected resource uri is not in resourceSubscriptions set
    - only show unsubscribe button if selected resource uri is in resourceSubscriptions set
    - wrap buttons in a flex div that is
      - justified right
      - has a minimal gap between
      - 2/5 wide (just big enough to contain two buttons and leave the h3 text 3/5 of the row to render and not overflow.
2025-03-08 13:40:37 -05:00
cliffhall
747c0154c5 WIP: Subscribe to resources
* In App.tsx
  - added subscribeToResource()
    - takes a uri
    - sends a `resources/subscribe` message with the uri
  - added unsubscribeFromResource()
    - takes a uri
    - sends a `resources/unsubscribe` message with the uri
  - in ResourcesTab element,
    - pass subscribeToResource and subscribeToResource invokers to component
* In notificationTypes.ts
  - add ServerNotificationSchema to NotificationSchema to permit server update messages.

* In ResourcesTab.tsx
  - deconstruct subscribeToResource and unsubscribeFromResource and add prop types
  - Add Subscribe and Unsubscribe buttons to selected resource panel, left of the refresh button. They call the sub and unsub functions that came in on props, passing the selected resource URI.
  - [WIP]: Will show the appropriate button in a follow up commit.
* In useConnection.ts
  - import ResourceUpdatedNotificationSchema
  - in the connect function,
    - set onNotification as the handler for ResourceUpdatedNotificationSchema
2025-03-08 11:05:13 -05:00
Ola Hungerford
0870a81990 Merge pull request #169 from cliffhall/dial-back-ping-button-energy
Removing the all the hype from the ping button.
2025-03-07 19:02:37 -07:00
cliffhall
ca18faa7c3 Removing the all the hype from the ping button.
Discussion at:
https://github.com/orgs/modelcontextprotocol/discussions/186
2025-03-07 13:05:45 -05:00
Ola Hungerford
014730fb2f Merge pull request #164 from TornjV/patch-1
Restore timeout search param
2025-03-06 10:22:33 -07:00
Veljko Tornjanski
9c690e004b Update useConnection.ts 2025-03-05 18:17:39 +01:00
11 changed files with 171 additions and 33 deletions

View File

@@ -38,6 +38,10 @@ CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build
For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).
### Authentication
The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header.
### From this repository ### From this repository
If you're working on the inspector itself: If you're working on the inspector itself:

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.5.1", "version": "0.6.0",
"description": "Client-side application for the Model Context Protocol inspector", "description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",

View File

@@ -97,6 +97,9 @@ const App = () => {
>([]); >([]);
const [roots, setRoots] = useState<Root[]>([]); const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({}); const [env, setEnv] = useState<Record<string, string>>({});
const [bearerToken, setBearerToken] = useState<string>(() => {
return localStorage.getItem("lastBearerToken") || "";
});
const [pendingSampleRequests, setPendingSampleRequests] = useState< const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array< Array<
@@ -128,6 +131,10 @@ const App = () => {
const [selectedResource, setSelectedResource] = useState<Resource | null>( const [selectedResource, setSelectedResource] = useState<Resource | null>(
null, null,
); );
const [resourceSubscriptions, setResourceSubscriptions] = useState<
Set<string>
>(new Set<string>());
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null); const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null); const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const [nextResourceCursor, setNextResourceCursor] = useState< const [nextResourceCursor, setNextResourceCursor] = useState<
@@ -160,6 +167,7 @@ const App = () => {
args, args,
sseUrl, sseUrl,
env, env,
bearerToken,
proxyServerUrl: PROXY_SERVER_URL, proxyServerUrl: PROXY_SERVER_URL,
onNotification: (notification) => { onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]); setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -195,6 +203,10 @@ const App = () => {
localStorage.setItem("lastTransportType", transportType); localStorage.setItem("lastTransportType", transportType);
}, [transportType]); }, [transportType]);
useEffect(() => {
localStorage.setItem("lastBearerToken", bearerToken);
}, [bearerToken]);
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
useEffect(() => { useEffect(() => {
const serverUrl = params.get("serverUrl"); const serverUrl = params.get("serverUrl");
@@ -308,6 +320,38 @@ const App = () => {
setResourceContent(JSON.stringify(response, null, 2)); setResourceContent(JSON.stringify(response, null, 2));
}; };
const subscribeToResource = async (uri: string) => {
if (!resourceSubscriptions.has(uri)) {
await makeRequest(
{
method: "resources/subscribe" as const,
params: { uri },
},
z.object({}),
"resources",
);
const clone = new Set(resourceSubscriptions);
clone.add(uri);
setResourceSubscriptions(clone);
}
};
const unsubscribeFromResource = async (uri: string) => {
if (resourceSubscriptions.has(uri)) {
await makeRequest(
{
method: "resources/unsubscribe" as const,
params: { uri },
},
z.object({}),
"resources",
);
const clone = new Set(resourceSubscriptions);
clone.delete(uri);
setResourceSubscriptions(clone);
}
};
const listPrompts = async () => { const listPrompts = async () => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -382,6 +426,8 @@ const App = () => {
setSseUrl={setSseUrl} setSseUrl={setSseUrl}
env={env} env={env}
setEnv={setEnv} setEnv={setEnv}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
onConnect={connectMcpServer} onConnect={connectMcpServer}
stdErrNotifications={stdErrNotifications} stdErrNotifications={stdErrNotifications}
/> />
@@ -485,6 +531,18 @@ const App = () => {
clearError("resources"); clearError("resources");
setSelectedResource(resource); setSelectedResource(resource);
}} }}
resourceSubscriptionsSupported={
serverCapabilities?.resources?.subscribe || false
}
resourceSubscriptions={resourceSubscriptions}
subscribeToResource={(uri) => {
clearError("resources");
subscribeToResource(uri);
}}
unsubscribeFromResource={(uri) => {
clearError("resources");
unsubscribeFromResource(uri);
}}
handleCompletion={handleCompletion} handleCompletion={handleCompletion}
completionsSupported={completionsSupported} completionsSupported={completionsSupported}
resourceContent={resourceContent} resourceContent={resourceContent}

View File

@@ -7,11 +7,9 @@ const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
<div className="col-span-2 flex justify-center items-center"> <div className="col-span-2 flex justify-center items-center">
<Button <Button
onClick={onPingClick} onClick={onPingClick}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white font-bold py-6 px-12 rounded-full shadow-lg transform transition duration-300 hover:scale-110 focus:outline-none focus:ring-4 focus:ring-purple-300 animate-pulse" className="font-bold py-6 px-12 rounded-full"
> >
<span className="text-3xl mr-2">🚀</span> Ping Server
MEGA PING
<span className="text-3xl ml-2">💥</span>
</Button> </Button>
</div> </div>
</TabsContent> </TabsContent>

View File

@@ -26,6 +26,10 @@ const ResourcesTab = ({
readResource, readResource,
selectedResource, selectedResource,
setSelectedResource, setSelectedResource,
resourceSubscriptionsSupported,
resourceSubscriptions,
subscribeToResource,
unsubscribeFromResource,
handleCompletion, handleCompletion,
completionsSupported, completionsSupported,
resourceContent, resourceContent,
@@ -52,6 +56,10 @@ const ResourcesTab = ({
nextCursor: ListResourcesResult["nextCursor"]; nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null; error: string | null;
resourceSubscriptionsSupported: boolean;
resourceSubscriptions: Set<string>;
subscribeToResource: (uri: string) => void;
unsubscribeFromResource: (uri: string) => void;
}) => { }) => {
const [selectedTemplate, setSelectedTemplate] = const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null); useState<ResourceTemplate | null>(null);
@@ -164,6 +172,29 @@ const ResourcesTab = ({
: "Select a resource or template"} : "Select a resource or template"}
</h3> </h3>
{selectedResource && ( {selectedResource && (
<div className="flex row-auto gap-1 justify-end w-2/5">
{resourceSubscriptionsSupported &&
!resourceSubscriptions.has(selectedResource.uri) && (
<Button
variant="outline"
size="sm"
onClick={() => subscribeToResource(selectedResource.uri)}
>
Subscribe
</Button>
)}
{resourceSubscriptionsSupported &&
resourceSubscriptions.has(selectedResource.uri) && (
<Button
variant="outline"
size="sm"
onClick={() =>
unsubscribeFromResource(selectedResource.uri)
}
>
Unsubscribe
</Button>
)}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -172,6 +203,7 @@ const ResourcesTab = ({
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />
Refresh Refresh
</Button> </Button>
</div>
)} )}
</div> </div>
<div className="p-4"> <div className="p-4">

View File

@@ -35,6 +35,8 @@ interface SidebarProps {
setSseUrl: (url: string) => void; setSseUrl: (url: string) => void;
env: Record<string, string>; env: Record<string, string>;
setEnv: (env: Record<string, string>) => void; setEnv: (env: Record<string, string>) => void;
bearerToken: string;
setBearerToken: (token: string) => void;
onConnect: () => void; onConnect: () => void;
stdErrNotifications: StdErrNotification[]; stdErrNotifications: StdErrNotification[];
} }
@@ -51,11 +53,14 @@ const Sidebar = ({
setSseUrl, setSseUrl,
env, env,
setEnv, setEnv,
bearerToken,
setBearerToken,
onConnect, onConnect,
stdErrNotifications, stdErrNotifications,
}: SidebarProps) => { }: SidebarProps) => {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false); const [showEnvVars, setShowEnvVars] = useState(false);
const [showBearerToken, setShowBearerToken] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set()); const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
return ( return (
@@ -110,6 +115,7 @@ const Sidebar = ({
</div> </div>
</> </>
) : ( ) : (
<>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">URL</label> <label className="text-sm font-medium">URL</label>
<Input <Input
@@ -119,6 +125,33 @@ const Sidebar = ({
className="font-mono" className="font-mono"
/> />
</div> </div>
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowBearerToken(!showBearerToken)}
className="flex items-center w-full"
>
{showBearerToken ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Authentication
</Button>
{showBearerToken && (
<div className="space-y-2">
<label className="text-sm font-medium">Bearer Token</label>
<Input
placeholder="Bearer Token"
value={bearerToken}
onChange={(e) => setBearerToken(e.target.value)}
className="font-mono"
type="password"
/>
</div>
)}
</div>
</>
)} )}
{transportType === "stdio" && ( {transportType === "stdio" && (
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -9,6 +9,7 @@ import {
CreateMessageRequestSchema, CreateMessageRequestSchema,
ListRootsRequestSchema, ListRootsRequestSchema,
ProgressNotificationSchema, ProgressNotificationSchema,
ResourceUpdatedNotificationSchema,
Request, Request,
Result, Result,
ServerCapabilities, ServerCapabilities,
@@ -26,7 +27,9 @@ import { Notification, StdErrNotificationSchema } from "../notificationTypes";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { authProvider } from "../auth"; import { authProvider } from "../auth";
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; const params = new URLSearchParams(window.location.search);
const DEFAULT_REQUEST_TIMEOUT_MSEC =
parseInt(params.get("timeout") ?? "") || 10000;
interface UseConnectionOptions { interface UseConnectionOptions {
transportType: "stdio" | "sse"; transportType: "stdio" | "sse";
@@ -35,6 +38,7 @@ interface UseConnectionOptions {
sseUrl: string; sseUrl: string;
env: Record<string, string>; env: Record<string, string>;
proxyServerUrl: string; proxyServerUrl: string;
bearerToken?: string;
requestTimeout?: number; requestTimeout?: number;
onNotification?: (notification: Notification) => void; onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void;
@@ -55,6 +59,7 @@ export function useConnection({
sseUrl, sseUrl,
env, env,
proxyServerUrl, proxyServerUrl,
bearerToken,
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC, requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
onNotification, onNotification,
onStdErrNotification, onStdErrNotification,
@@ -226,9 +231,11 @@ export function useConnection({
// Inject auth manually instead of using SSEClientTransport, because we're // Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first. // proxying through the inspector server first.
const headers: HeadersInit = {}; const headers: HeadersInit = {};
const tokens = await authProvider.tokens();
if (tokens) { // Use manually provided bearer token if available, otherwise use OAuth tokens
headers["Authorization"] = `Bearer ${tokens.access_token}`; const token = bearerToken || (await authProvider.tokens())?.access_token;
if (token) {
headers["Authorization"] = `Bearer ${token}`;
} }
const clientTransport = new SSEClientTransport(backendUrl, { const clientTransport = new SSEClientTransport(backendUrl, {
@@ -245,6 +252,11 @@ export function useConnection({
ProgressNotificationSchema, ProgressNotificationSchema,
onNotification, onNotification,
); );
client.setNotificationHandler(
ResourceUpdatedNotificationSchema,
onNotification,
);
} }
if (onStdErrNotification) { if (onStdErrNotification) {

View File

@@ -1,6 +1,7 @@
import { import {
NotificationSchema as BaseNotificationSchema, NotificationSchema as BaseNotificationSchema,
ClientNotificationSchema, ClientNotificationSchema,
ServerNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod"; import { z } from "zod";
@@ -13,7 +14,7 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({
export const NotificationSchema = ClientNotificationSchema.or( export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema, StdErrNotificationSchema,
); ).or(ServerNotificationSchema);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>; export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>; export type Notification = z.infer<typeof NotificationSchema>;

4
package-lock.json generated
View File

@@ -13,8 +13,8 @@
"server" "server"
], ],
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-client": "0.4.1", "@modelcontextprotocol/inspector-client": "^0.5.1",
"@modelcontextprotocol/inspector-server": "0.4.1", "@modelcontextprotocol/inspector-server": "^0.5.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"spawn-rx": "^5.1.2", "spawn-rx": "^5.1.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.5.1", "version": "0.6.0",
"description": "Model Context Protocol inspector", "description": "Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@@ -34,8 +34,8 @@
"publish-all": "npm publish --workspaces --access public && npm publish --access public" "publish-all": "npm publish --workspaces --access public && npm publish --access public"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-client": "0.4.1", "@modelcontextprotocol/inspector-client": "^0.6.0",
"@modelcontextprotocol/inspector-server": "0.4.1", "@modelcontextprotocol/inspector-server": "^0.6.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"spawn-rx": "^5.1.2", "spawn-rx": "^5.1.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.5.1", "version": "0.6.0",
"description": "Server-side application for the Model Context Protocol inspector", "description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",