Promote an allowlisted MCP prompt to a slash command, and decide how it answers inside Discord's deadline #884

Open
opened 2026-08-17 04:15:47 +00:00 by coilyco-ops · 0 comments
Member

Split from #127 by Angie (ENG, claude seat) while building the registration half. #127 asked for MCP prompts as Discord slash commands and the registration prerequisite turned out to be missing entirely, so that shipped on its own. This is the remaining half, and it carries a problem nothing on #127 has named yet.

What is already decided, so this is not re-litigating

  • The allowlist, decided on #127 on 2026-08-12. "Each promotable prompt is named here, with its command's argument schema declared here alongside it." A conservative default bound and an auto-derived per-prompt declaration were both offered and rejected, because a schema the publishing server supplies is the bounded party describing its own limits.
  • The access policy, settled in 3142129. An interaction takes the same access.Evaluate, the same allowlist, and the same admission budget keyed on origin.Key() that a mention does.
  • Registration, now built, per guild and bulk-overwrite, gated on SIRENS_ECHO_DISCORD_COMMANDS.

What is built and reusable

  • CommandFromPrompt in promptcommand.go maps an mcp.Prompt and its PromptArgument set onto Discord's shape, refusing rather than repairing.
  • MCPProvider.Prompt(ctx, server, name, arguments) fetches a prompt and returns []PromptMessage, checking required arguments before the call.
  • seedFromPrompt in http.go folds those messages into a turn as conversation. The HTTP surface has selected prompts this way since 4d5fb97.

So the pieces exist. Assembling them is not the hard part.

The hard part, which is new information

An interaction must be answered within three seconds. A model turn takes minutes.

respondToCommand answers immediately with InteractionResponseChannelMessageWithSource. Every command that exists today fits: /job-status reads a record, /mcps renders a roster, /echo submits a job and returns an id. A prompt command is the first one whose whole purpose is to produce a model reply, and it cannot answer in three seconds.

Measured, on #577: Echo's round 0 alone ranged 133.5s to 255.4s on comparable work.

Two shapes, and picking between them is the decision this issue needs:

  1. Defer the interaction. InteractionResponseDeferredChannelMessageWithSource buys 15 minutes, then a follow-up edit carries the reply. Closest to what a member expects from a slash command, and it puts a minutes-long turn on an interaction token with its own expiry and its own failure mode.
  2. Submit a job. Reuses the async machinery that already exists - thread binding, progress, the terminal notice - and matches how /echo already behaves. Needs a job kind that runs a prompt-seeded turn, and the member gets a job id first rather than an answer.

Option 2 is more consistent with the surface as built. Option 1 is more consistent with what a slash command feels like. That is a product call rather than an engineering one, which is why this is consult rather than headless.

Also unanswered

  • prompts/list_changed. The handler exists at mcp.go:390. Re-registering on every change is a Discord write on a server's schedule, which is a rate-limit surface. Re-registering never means an allowlisted prompt that appears later stays dark until a restart.
  • Discord's command-count limit if the allowlist ever grows past it, noted in #127's own scope.
  • What the allowlist should contain. Nothing has named a promotable prompt yet. Shipping it empty is correct - a prompt is not a command until someone here says so - but the feature does nothing until an entry exists, and choosing the first one wants a person.

Acceptance

  • PromptCommands() exists as a closed allowlist, empty by default, each entry naming a server, a prompt, and a locally declared argument schema.
  • An allowlisted prompt is registered alongside the declared set, and an entry naming a prompt no server publishes is refused at boot rather than registered dark.
  • Invoking one routes through the same admission, grounding, and response validation as every other ingress, and answers within Discord's deadline by whichever of the two shapes is chosen.
  • The schema comes from here, never from the published prompt.

Related - #127 (the parent, and the decision record), #163 (closed - boot-time discovery, which the introducing commit cited as a blocker).

Split from #127 by Angie (ENG, `claude` seat) while building the registration half. #127 asked for MCP prompts as Discord slash commands and the registration prerequisite turned out to be missing entirely, so that shipped on its own. This is the remaining half, and it carries a problem nothing on #127 has named yet. ## What is already decided, so this is not re-litigating * **The allowlist**, decided on #127 on 2026-08-12. *"Each promotable prompt is named here, with its command's argument schema declared here alongside it."* A conservative default bound and an auto-derived per-prompt declaration were both offered and rejected, because a schema the publishing server supplies is the bounded party describing its own limits. * **The access policy**, settled in `3142129`. An interaction takes the same `access.Evaluate`, the same allowlist, and the same admission budget keyed on `origin.Key()` that a mention does. * **Registration**, now built, per guild and bulk-overwrite, gated on `SIRENS_ECHO_DISCORD_COMMANDS`. ## What is built and reusable * `CommandFromPrompt` in `promptcommand.go` maps an `mcp.Prompt` and its `PromptArgument` set onto Discord's shape, refusing rather than repairing. * `MCPProvider.Prompt(ctx, server, name, arguments)` fetches a prompt and returns `[]PromptMessage`, checking required arguments before the call. * `seedFromPrompt` in `http.go` folds those messages into a turn as conversation. The HTTP surface has selected prompts this way since `4d5fb97`. So the pieces exist. Assembling them is not the hard part. ## The hard part, which is new information **An interaction must be answered within three seconds. A model turn takes minutes.** `respondToCommand` answers immediately with `InteractionResponseChannelMessageWithSource`. Every command that exists today fits: `/job-status` reads a record, `/mcps` renders a roster, `/echo` submits a job and returns an id. **A prompt command is the first one whose whole purpose is to produce a model reply**, and it cannot answer in three seconds. Measured, on #577: Echo's round 0 alone ranged **133.5s to 255.4s** on comparable work. Two shapes, and picking between them is the decision this issue needs: 1. **Defer the interaction.** `InteractionResponseDeferredChannelMessageWithSource` buys 15 minutes, then a follow-up edit carries the reply. Closest to what a member expects from a slash command, and it puts a minutes-long turn on an interaction token with its own expiry and its own failure mode. 2. **Submit a job.** Reuses the async machinery that already exists - thread binding, progress, the terminal notice - and matches how `/echo` already behaves. Needs a job kind that runs a prompt-seeded turn, and the member gets a job id first rather than an answer. Option 2 is more consistent with the surface as built. Option 1 is more consistent with what a slash command feels like. That is a product call rather than an engineering one, which is why this is `consult` rather than `headless`. ## Also unanswered * **`prompts/list_changed`.** The handler exists at `mcp.go:390`. Re-registering on every change is a Discord write on a server's schedule, which is a rate-limit surface. Re-registering never means an allowlisted prompt that appears later stays dark until a restart. * **Discord's command-count limit** if the allowlist ever grows past it, noted in #127's own scope. * **What the allowlist should contain.** Nothing has named a promotable prompt yet. Shipping it empty is correct - a prompt is not a command until someone here says so - but the feature does nothing until an entry exists, and choosing the first one wants a person. ## Acceptance * `PromptCommands()` exists as a closed allowlist, empty by default, each entry naming a server, a prompt, and a locally declared argument schema. * An allowlisted prompt is registered alongside the declared set, and an entry naming a prompt no server publishes is refused at boot rather than registered dark. * Invoking one routes through the same admission, grounding, and response validation as every other ingress, and answers within Discord's deadline by whichever of the two shapes is chosen. * The schema comes from here, never from the published prompt. **Related** - #127 (the parent, and the decision record), #163 (closed - boot-time discovery, which the introducing commit cited as a blocker).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#884
No description provided.