Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
165 changes: 84 additions & 81 deletions client/src/components/PromptsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useEffect, useMemo, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Textarea } from "./ui/textarea";
Expand All @@ -20,47 +20,39 @@ import { MessageSquare, Play, RefreshCw, ChevronRight } from "lucide-react";
import { EmptyState } from "./ui/empty-state";
import JsonView from "react18-json-view";
import "react18-json-view/src/style.css";
import { MCPServerConfig } from "@/shared/mcp-client-manager";

interface Prompt {
name: string;
description?: string;
version?: string;
arguments?: {
name: string;
description?: string;
required?: boolean;
}[];
}
import { MCPServerConfig, type MCPPrompt } from "@/shared/mcp-client-manager";

interface PromptsTabProps {
serverConfig?: MCPServerConfig;
serverName?: string;
}

type PromptArgument = NonNullable<MCPPrompt["arguments"]>[number];

interface FormField {
name: string;
type: string;
description?: string;
required: boolean;
value: any;
value: string | boolean;
enum?: string[];
minimum?: number;
maximum?: number;
}

export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
const [prompts, setPrompts] = useState<Record<string, Prompt[]>>({});
const [prompts, setPrompts] = useState<MCPPrompt[]>([]);
const [selectedPrompt, setSelectedPrompt] = useState<string>("");
const [selectedPromptData, setSelectedPromptData] = useState<Prompt | null>(
null,
);
const [formFields, setFormFields] = useState<FormField[]>([]);
const [promptContent, setPromptContent] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [fetchingPrompts, setFetchingPrompts] = useState(false);
const [error, setError] = useState<string>("");

const selectedPromptData = useMemo(() => {
return prompts.find((prompt) => prompt.name === selectedPrompt) ?? null;
}, [prompts, selectedPrompt]);

useEffect(() => {
if (serverConfig && serverName) {
fetchPrompts();
Expand Down Expand Up @@ -91,18 +83,31 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
const data = await response.json();

if (response.ok) {
setPrompts(data.prompts || {});
const serverPrompts: MCPPrompt[] = Array.isArray(data.prompts)
? data.prompts
: [];
setPrompts(serverPrompts);

if (serverPrompts.length === 0) {
setSelectedPrompt("");
setPromptContent(null);
} else if (
!serverPrompts.some((prompt) => prompt.name === selectedPrompt)
) {
setSelectedPrompt(serverPrompts[0].name);
setPromptContent(null);
}
} else {
setError(data.error || "Failed to fetch prompts");
setError(data.error || "Could not fetch prompts");
}
} catch (err) {
setError("Network error fetching prompts");
setError(`Could not fetch prompts: ${err}`);
} finally {
setFetchingPrompts(false);
}
};

const generateFormFields = (args: any[]) => {
const generateFormFields = (args: PromptArgument[]) => {
if (!args || args.length === 0) {
setFormFields([]);
return;
Expand All @@ -112,41 +117,42 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
name: arg.name,
type: "string", // Default to string for now, could be enhanced based on arg type
description: arg.description,
required: arg.required || false,
required: Boolean(arg.required),
value: "",
}));

setFormFields(fields);
};

const updateFieldValue = (fieldName: string, value: any) => {
const updateFieldValue = (fieldName: string, value: string | boolean) => {
setFormFields((prev) =>
prev.map((field) =>
field.name === fieldName ? { ...field, value } : field,
),
);
};

const buildParameters = (): Record<string, any> => {
const params: Record<string, any> = {};
const buildParameters = (): Record<string, string> => {
const params: Record<string, string> = {};
formFields.forEach((field) => {
if (
field.value !== "" &&
field.value !== null &&
field.value !== undefined
) {
let processedValue = field.value;
let processedValue: string;

if (field.type === "number" || field.type === "integer") {
processedValue = Number(field.value);
if (field.type === "array" || field.type === "object") {
processedValue =
typeof field.value === "string"
? field.value
: JSON.stringify(field.value);
} else if (field.type === "boolean") {
processedValue = Boolean(field.value);
} else if (field.type === "array" || field.type === "object") {
try {
processedValue = JSON.parse(field.value);
} catch {
processedValue = field.value;
}
processedValue = field.value ? "true" : "false";
} else if (field.type === "number" || field.type === "integer") {
processedValue = String(field.value);
} else {
processedValue = String(field.value);
}

params[field.name] = processedValue;
Expand Down Expand Up @@ -187,9 +193,7 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
}
};

const promptNames = Object.values(prompts)
.flat()
.map((prompt) => prompt.name);
const promptNames = prompts.map((prompt) => prompt.name);

if (!serverConfig || !serverName) {
return (
Expand Down Expand Up @@ -257,47 +261,44 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
</div>
) : (
<div className="space-y-1">
{promptNames.map((name) => (
<div
key={name}
className={`cursor-pointer transition-all duration-200 hover:bg-muted/30 dark:hover:bg-muted/50 p-3 rounded-md mx-2 ${
selectedPrompt === name
? "bg-muted/50 dark:bg-muted/50 shadow-sm border border-border ring-1 ring-ring/20"
: "hover:shadow-sm"
}`}
onClick={() => {
setSelectedPrompt(name);
const promptData = Object.values(prompts)
.flat()
.find((p) => p.name === name);
setSelectedPromptData(promptData || null);
}}
>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<code className="font-mono text-xs font-medium text-foreground bg-muted px-1.5 py-0.5 rounded border border-border">
{name}
</code>
{prompts.map((prompt) => {
const isSelected = selectedPrompt === prompt.name;
const displayTitle = prompt.title ?? prompt.name;
return (
<div
key={prompt.name}
className={`cursor-pointer transition-all duration-200 hover:bg-muted/30 dark:hover:bg-muted/50 p-3 rounded-md mx-2 ${
isSelected
? "bg-muted/50 dark:bg-muted/50 shadow-sm border border-border ring-1 ring-ring/20"
: "hover:shadow-sm"
}`}
onClick={() => {
setSelectedPrompt(prompt.name);
}}
>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<code className="font-mono text-xs font-medium text-foreground bg-muted px-1.5 py-0.5 rounded border border-border">
{prompt.name}
</code>
{prompt.title && (
<span className="text-xs font-semibold text-foreground">
{displayTitle}
</span>
)}
</div>
{prompt.description && (
<p className="text-xs mt-2 line-clamp-2 leading-relaxed text-muted-foreground">
{prompt.description}
</p>
)}
</div>
{Object.values(prompts)
.flat()
.find((p) => p.name === name)
?.description && (
<p className="text-xs mt-2 line-clamp-2 leading-relaxed text-muted-foreground">
{
Object.values(prompts)
.flat()
.find((p) => p.name === name)
?.description
}
</p>
)}
<ChevronRight className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1" />
</div>
<ChevronRight className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1" />
</div>
</div>
))}
);
})}
</div>
)}
</div>
Expand Down Expand Up @@ -403,7 +404,7 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
<div className="space-y-2">
{field.type === "enum" ? (
<Select
value={field.value}
value={String(field.value)}
onValueChange={(value) =>
updateFieldValue(field.name, value)
}
Expand All @@ -430,7 +431,7 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
<div className="flex items-center space-x-3 py-2">
<input
type="checkbox"
checked={field.value}
checked={field.value === true}
onChange={(e) =>
updateFieldValue(
field.name,
Expand All @@ -440,7 +441,9 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
className="w-4 h-4 text-primary bg-background border-border rounded focus:ring-ring focus:ring-2"
/>
<span className="text-xs text-foreground font-medium">
{field.value ? "Enabled" : "Disabled"}
{field.value === true
? "Enabled"
: "Disabled"}
</span>
</div>
) : field.type === "array" ||
Expand Down Expand Up @@ -472,7 +475,7 @@ export function PromptsTab({ serverConfig, serverName }: PromptsTabProps) {
? "number"
: "text"
}
value={field.value}
value={String(field.value)}
onChange={(e) =>
updateFieldValue(
field.name,
Expand Down
33 changes: 19 additions & 14 deletions server/routes/mcp/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ const prompts = new Hono();
// List prompts endpoint
prompts.post("/list", async (c) => {
try {
const { serverId } = await c.req.json();
const { serverId } = (await c.req.json()) as { serverId?: string };

if (!serverId) {
return c.json({ success: false, error: "serverId is required" }, 400);
}

const mcpJamClientManager = c.mcpJamClientManager;

// Get prompts for specific server
const serverPrompts = mcpJamClientManager.getPromptsForServer(serverId);

return c.json({ prompts: { [serverId]: serverPrompts } });
const mcpClientManager = c.mcpClientManager;
const { prompts } = await mcpClientManager.listPrompts(serverId);
return c.json({ prompts });
} catch (error) {
console.error("Error fetching prompts:", error);
return c.json(
Expand All @@ -33,7 +30,11 @@ prompts.post("/list", async (c) => {
// Get prompt endpoint
prompts.post("/get", async (c) => {
try {
const { serverId, name, args } = await c.req.json();
const { serverId, name, args } = (await c.req.json()) as {
serverId?: string;
name?: string;
args?: Record<string, unknown>;
};

if (!serverId) {
return c.json({ success: false, error: "serverId is required" }, 400);
Expand All @@ -49,14 +50,18 @@ prompts.post("/get", async (c) => {
);
}

const mcpJamClientManager = c.mcpJamClientManager;
const mcpClientManager = c.mcpClientManager;

// Get prompt content directly - servers are already connected
const content = await mcpJamClientManager.getPrompt(
const promptArguments = args
? Object.fromEntries(
Object.entries(args).map(([key, value]) => [key, String(value)]),
)
: undefined;

const content = await mcpClientManager.getPrompt(serverId, {
name,
serverId,
args || {},
);
arguments: promptArguments,
});

return c.json({ content });
} catch (error) {
Expand Down
13 changes: 7 additions & 6 deletions server/routes/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from "hono";
import type {
ElicitRequest,
ElicitResult,
ListToolsResult,
} from "@modelcontextprotocol/sdk/types.js";
import "../../types/hono"; // Type extensions

Expand All @@ -19,9 +20,7 @@ type ExecutionContext = {
id: string;
serverId: string;
toolName: string;
execPromise: Promise<
CallToolResultSchema | CompatibilityCallToolResultSchema
>;
execPromise: Promise<ListToolsResult>;
queue: ElicitationPayload[];
waiter?: (payload: ElicitationPayload) => void;
};
Expand Down Expand Up @@ -136,9 +135,11 @@ tools.post("/execute", async (c) => {
id: executionId,
serverId,
toolName,
execPromise: manager.executeTool(serverId, toolName, parameters) as Promise<
CallToolResultSchema | CompatibilityCallToolResultSchema
>,
execPromise: manager.executeTool(
serverId,
toolName,
parameters,
) as Promise<ListToolsResult>,
queue: [],
};

Expand Down
Loading