Skip to main content
Glama

send

Send messages to the most recent OpenCode session on an instance, receiving real‑time streamed responses. Optionally abort a running task instead of sending a message.

Instructions

Send a message to the most recent opencode session on an instance. Streams the response back in real-time. Set abort=true to stop a running task instead of sending a message.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
messageNoThe message to send (not required when aborting)
instanceYesInstance name (exact or fuzzy substring match)
abortNoSet to true to abort the currently running task

Implementation Reference

  • The 'send' tool handler: registers an MCP tool named 'send' with schema (message, instance, abort) and handler logic that handles abort mode, message sending, SSE streaming, and response collection.
    server.tool(
      'send',
      'Send a message to the most recent opencode session on an instance. Streams the response back in real-time. Set abort=true to stop a running task instead of sending a message.',
      {
        message: z
          .string()
          .optional()
          .describe('The message to send (not required when aborting)'),
        instance: z
          .string()
          .describe('Instance name (exact or fuzzy substring match)'),
        abort: z
          .boolean()
          .optional()
          .default(false)
          .describe('Set to true to abort the currently running task'),
      },
      async ({ message, instance: query, abort }, extra) => {
        try {
          const { instance } = registry.resolveInstance(query)
          const baseUrl = instance.url
    
          // Abort mode
          if (abort) {
            const { busySessionId } = await getInstanceStatus(baseUrl)
            if (!busySessionId) {
              return {
                content: [
                  {
                    type: 'text',
                    text: `No active task on ${instance.name} — nothing to abort.`,
                  },
                ],
              }
            }
            await fetch(`${baseUrl}/session/${busySessionId}/abort`, {
              method: 'POST',
            })
            return {
              content: [
                {
                  type: 'text',
                  text: `Aborted active task on ${instance.name}.`,
                },
              ],
            }
          }
    
          // Send mode — message is required
          if (!message) {
            return {
              content: [
                {
                  type: 'text',
                  text: 'Error: message is required when not aborting.',
                },
              ],
              isError: true,
            }
          }
    
          // Check if instance is busy
          const { busySessionId } = await getInstanceStatus(baseUrl)
          if (busySessionId) {
            return {
              content: [
                {
                  type: 'text',
                  text:
                    `${instance.name} is currently busy processing a task. ` +
                    `You can wait for it to finish, or call send with abort=true to stop it.`,
                },
              ],
            }
          }
    
          // Find the most recently updated session
          const session = await findMostRecentSession(baseUrl)
          if (!session) {
            return {
              content: [
                {
                  type: 'text',
                  text: `No sessions found on ${instance.name}. Open opencode and start a session first.`,
                },
              ],
              isError: true,
            }
          }
    
          // Submit via session API
          await fetch(`${baseUrl}/session/${session.id}/prompt_async`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              parts: [{ type: 'text', text: message }],
            }),
          })
    
          // Subscribe to SSE and stream deltas back
          const timeout = Number(process.env.SEND_TIMEOUT_MS) || 300_000
          const controller = new AbortController()
          const timer = setTimeout(() => controller.abort(), timeout)
    
          let fullResponse = ''
    
          try {
            for await (const event of sseEvents(
              baseUrl,
              controller.signal,
            )) {
              if (
                event.type === 'message.part.delta' &&
                event.properties.field === 'text' &&
                event.properties.sessionID === session.id
              ) {
                const delta = event.properties.delta as string
                fullResponse += delta
    
                // Stream delta back to MCP client via progress notification
                if (extra._meta?.progressToken !== undefined) {
                  await extra.sendNotification({
                    method: 'notifications/progress',
                    params: {
                      progressToken: extra._meta.progressToken,
                      progress: fullResponse.length,
                      total: 0,
                      message: delta,
                    },
                  })
                }
              }
    
              if (
                event.type === 'session.idle' &&
                event.properties.sessionID === session.id
              ) {
                break
              }
            }
          } catch (err) {
            if ((err as Error).name === 'AbortError') {
              fullResponse +=
                '\n\n(still processing — timed out waiting for response)'
            } else {
              throw err
            }
          } finally {
            clearTimeout(timer)
            controller.abort()
          }
    
          if (!fullResponse) {
            return {
              content: [
                {
                  type: 'text',
                  text: `Message sent to ${instance.name}, but no response received. Check the instance directly.`,
                },
              ],
            }
          }
    
          return {
            content: [
              {
                type: 'text',
                text: fullResponse,
              },
            ],
          }
        } catch (err) {
          return {
            content: [
              { type: 'text', text: `Error: ${(err as Error).message}` },
            ],
            isError: true,
          }
        }
      },
    )
  • Input schema for the 'send' tool: optional message string, required instance string, optional abort boolean (default false).
    {
      message: z
        .string()
        .optional()
        .describe('The message to send (not required when aborting)'),
      instance: z
        .string()
        .describe('Instance name (exact or fuzzy substring match)'),
      abort: z
        .boolean()
        .optional()
        .default(false)
        .describe('Set to true to abort the currently running task'),
    },
  • src/index.ts:38-38 (registration)
    Registration entry point: calls registerSimplifiedTools(server, registry) which registers the 'send' tool alongside other tools.
    registerSimplifiedTools(server, registry)
  • Function signature that registers all simplified tools including 'send' via server.tool().
    export function registerSimplifiedTools(
      server: McpServer,
      registry: InstanceRegistry,
    ): void {
  • sseEvents helper: async generator subscribing to SSE event stream, used by the 'send' handler to stream response deltas back in real-time.
    /**
     * Subscribe to an opencode instance's SSE event stream.
     * Returns an async iterator of parsed events.
     */
    async function* sseEvents(
      baseUrl: string,
      signal: AbortSignal,
    ): AsyncGenerator<{ type: string; properties: Record<string, unknown> }> {
      const res = await fetch(`${baseUrl}/event`, {
        headers: { Accept: 'text/event-stream' },
        signal,
      })
    
      if (!res.ok || !res.body) return
    
      const reader = res.body.getReader()
      const decoder = new TextDecoder()
      let buffer = ''
    
      try {
        while (true) {
          const { done, value } = await reader.read()
          if (done) break
    
          buffer += decoder.decode(value, { stream: true })
          const lines = buffer.split('\n')
          buffer = lines.pop() ?? ''
    
          for (const line of lines) {
            if (line.startsWith('data: ')) {
              const data = line.slice(6).trim()
              if (data) {
                try {
                  yield JSON.parse(data)
                } catch {
                  // Skip malformed events
                }
              }
            }
          }
        }
      } finally {
        reader.releaseLock()
      }
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Despite no annotations, the description discloses streaming response and abort behavior. However, it omits potential side effects, permission requirements, and details on automatic session selection.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise two sentences; first states primary function, second adds edge case. No wasted words, though structure could be slightly improved.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers basic use and abort but omits error handling, return format, and session selection mechanism, which are relevant for correct usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema provides 100% coverage, so baseline 3 applies. The description adds a note on abort usage but no additional semantics for message or instance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action (send a message), the target (opencode session on an instance), and streaming behavior. It implicitly distinguishes from siblings by focusing on messaging vs. reading/listing, but could be more explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit guidance on when to use this tool versus siblings 'instances' and 'read'. The description only mentions basic functionality and an abort case, but not selection criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/klutometis/opencode-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server