Skip to content

implemented MCP tool call commands#33

Merged
abubahmed merged 1 commit intostagingfrom
feat/add-commands
Apr 15, 2026
Merged

implemented MCP tool call commands#33
abubahmed merged 1 commit intostagingfrom
feat/add-commands

Conversation

@abubahmed
Copy link
Copy Markdown
Collaborator

Summary

  • Add slash command system that lets users type / to discover and invoke MCP tools directly from the chat input
  • Backend /commands/list endpoint fetches available tools from connected MCP servers and returns them as slash commands
  • Commands are persisted to Convex (commands table) with upsert logic so the list stays in sync with MCP servers
  • Frontend useSlashCommandInput hook handles command parsing, filtering, keyboard navigation (Arrow keys, Enter, Tab, Escape), and forced tool call dispatch via the existing chat stream

How it works

  1. On harness load, the frontend calls /commands/list with the harness's MCP server config
  2. The backend connects to each MCP server, lists available tools, and returns them as commands
  3. Commands are upserted into Convex and rendered in a dropdown when the user types /
  4. Selecting a command (click, Enter, or Tab) autofills /{tool_name} — the user adds a message and sends
  5. On send, the chat stream receives a forced_tool parameter so OpenRouter invokes the correct MCP tool

Files changed

  • Frontend: slash-commands.tsx (new), mcp.ts, use-chat-stream.ts, chat/index.tsx, harnesses/$harnessId.tsx, onboarding.tsx, mcp-server-status.tsx
  • Backend (FastAPI): routes/commands.py (new), routes/chat.py, services/openrouter.py, models.py, main.py
  • Backend (Convex): commands.ts (new), schema.ts, harnesses.ts

Test plan

  • Type / in chat input — dropdown appears with available MCP tools
  • Arrow keys navigate the list, Enter/Tab selects the highlighted command
  • Selecting a command autofills it and closes the dropdown
  • Typing an exact command name (e.g. /search_courses) closes the dropdown
  • Pressing Enter after command is selected sends the message and triggers the MCP tool call
  • Commands persist across page reloads (stored in Convex)
  • Harnesses with no MCP servers show "No MCP tools available" in the dropdown

@abubahmed
Copy link
Copy Markdown
Collaborator Author

@claude review this PR

@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an MCP-powered slash command system that lets users discover and invoke MCP tools via /... in the chat input, including a backend tool-listing endpoint, Convex persistence, and a frontend autocomplete/menu + forced tool dispatch flow.

Changes:

  • Add FastAPI /api/commands/list endpoint to list available MCP tools as slash commands.
  • Persist commands in Convex and associate command IDs to harness MCP servers.
  • Add frontend slash-command UI/hook and send forced_tool through the chat stream to force a specific tool on the first iteration.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
packages/fastapi/app/services/openrouter.py Adds optional tool_choice override support for OpenRouter requests.
packages/fastapi/app/routes/commands.py New endpoint that converts MCP tools into slash-command payloads.
packages/fastapi/app/routes/chat.py Supports forced_tool by setting OpenRouter tool_choice on the first iteration.
packages/fastapi/app/models.py Adds forced_tool to chat requests and a request model for /commands/list.
packages/fastapi/app/main.py Registers the new commands router.
packages/convex-backend/convex/schema.ts Adds commands table and stores commandIds on harness MCP server entries.
packages/convex-backend/convex/harnesses.ts Updates MCP server validator to include optional commandIds.
packages/convex-backend/convex/commands.ts New Convex functions to upsert commands and fetch by IDs.
packages/convex-backend/convex/_generated/api.d.ts Adds generated API typings for the new commands module.
apps/web/src/routes/onboarding.tsx Syncs commands after harness creation and stores command IDs on MCP servers.
apps/web/src/routes/harnesses/$harnessId.tsx Syncs commands after MCP server changes and stores command IDs on MCP servers.
apps/web/src/routes/chat/index.tsx Loads stored commands, adds slash-command UI, and forwards forced_tool during streaming.
apps/web/src/lib/use-chat-stream.ts Adds forced_tool to the stream request type.
apps/web/src/lib/mcp.ts Adds types/utilities for command fetching and MCP server payload shaping.
apps/web/src/components/slash-commands.tsx New hook + menu component implementing slash-command autocomplete and parsing.
apps/web/src/components/mcp-server-status.tsx Triggers command refresh after OAuth reconnect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4 to +8
/**
* Upsert commands: insert new ones, update existing ones (matched by name + userId).
* Returns an array of command IDs in the same order as the input.
*/
export const upsert = mutation({
Comment on lines +27 to +31
ctx.db
.query("commands")
.withIndex("by_name", (q) => q.eq("name", cmd.name))
.unique(),
),
Comment on lines +571 to +577
# Force a specific tool on the first iteration when forced_tool is set
tool_choice: dict | str | None = None
if body.forced_tool and iteration == 0 and tools:
tool_choice = {
"type": "function",
"function": {"name": body.forced_tool},
}

const selectCommand = useCallback(
(cmd: SlashCommand) => {
setText(`/${cmd.tool} `);
Comment on lines +32 to +39
if (afterSlash === cmd.tool || afterSlash.startsWith(`${cmd.tool} `)) {
return {
toolName: cmd.name,
message: afterSlash.slice(cmd.tool.length).trim(),
};
}
}

Comment on lines +3436 to 3440
// For slash commands, send the cleaned message (without /command prefix) to the LLM
const llmContent = messageContent;

// Add the new user message (with any current attachments)
if (readyAttachments.length > 0) {
Comment on lines +49 to +54
name: v.string(),
server: v.string(),
tool: v.string(),
description: v.string(),
parametersJson: v.string(),
}).index("by_name", ["name"]),
Comment on lines +56 to +60
export const getByIds = query({
args: { ids: v.array(v.id("commands")) },
handler: async (ctx, args) => {
const results = await Promise.all(args.ids.map((id) => ctx.db.get(id)));
return results.filter(Boolean);
Comment thread apps/web/src/lib/mcp.ts
Comment on lines +222 to +223
* Returns the raw command list with $-prefixed keys stripped from parameters,
* or null if the fetch fails.
Comment on lines +1171 to +1174
tool: c?.tool,
description: c?.description,
parameters: JSON.parse(c?.parametersJson),
}))}
@abubahmed abubahmed merged commit 3b1d095 into staging Apr 15, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants