Docs for agents

canhelpto is a help desk an agent can use directly. Three REST calls open, read and answer a ticket in a vendor's workspace; an MCP server wraps them as three tools. Machine-readable: openapi.json and llms.txt.

The contract

Step by step

  1. 1. Get a key

    Sign in at canhelpto.com/login, create a workspace for your product. The public API key is the widget id printed in your pages; generate a service key (Settings, Service key) for server-side calls: it is the one that opens service mode and the team connector.

  2. 2. Open a ticket

    One POST. Put the customer's words in message, then the context your server has under them. The same data goes in metadata.mcp so it comes back on read. Send X-Service-Mode: true because a server is posting on the customer's behalf.

  3. 3. Read it back

    GET the ticket without service mode. The reply array holds your team's public answers (userId set) and the customer's own (userId null). Before showing it, check that the ticket is the caller's: metadata.mcp.account.userId, or customerEmail.

  4. 4. Answer from rules first

    support_ask should not call a model. Match the question against the rules you already know (credits, connection, rate limit, permissions, sample sizes) and answer with the tool the agent should call next. An unmatched question points at support_open_ticket; never invent.

  5. 5. Tell the agent when to call

    Add one sentence to your server's instructions (MCP initialize) and a support hint to every error body: 'If this keeps failing, call support_ask; support_open_ticket puts it to the team with this error attached.'

Open a ticket

curl -X POST https://canhelpto.com/api/v1/tickets \
  -H "X-API-Key: $CANHELPTO_API_KEY" \
  -H "X-Service-Mode: true" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "approve_drafts fails with FORBIDDEN",
    "message": "Claude cannot approve my drafts.\n\n- Connector context -\nAccount: [email protected] · user 7f3e…\nClient: claude-user/1.0 · catalogue 1.8.0\nLast calls:\n- 16:05:00 approve_drafts · 120 ms · FORBIDDEN: managed account",
    "customerName": "Ana Pop",
    "customerEmail": "[email protected]",
    "priority": "HIGH",
    "category": "bug",
    "tags": ["mcp", "claude-connector", "bug"],
    "page": "mcp://your-server",
    "metadata": { "mcp": { "account": { "userId": "7f3e…" }, "client": { "userAgent": "claude-user/1.0" }, "recentCalls": [] } }
  }'
# 201 {"success":true,"ticket":{"id":"cmg5…","status":"OPEN","priority":"HIGH","source":"API"}}

Read it back

curl https://canhelpto.com/api/v1/tickets/cmg5… -H "X-API-Key: $CANHELPTO_API_KEY"
# no X-Service-Mode: internal notes stay out.
# Check metadata.mcp.account.userId (or customerEmail) against the caller before showing it.
curl "https://canhelpto.com/api/v1/tickets?email=ana%40example.com&limit=10" -H "X-API-Key: $CANHELPTO_API_KEY"

The MCP wrapper (TypeScript)

The shape of the three tools on the official SDK. Python (FastMCP) follows the same contract; any framework that can make three HTTP calls can do this.

// TypeScript, @modelcontextprotocol/sdk. The reference implementation runs on
// the Gigi connector; the npm package (@canhelpto/mcp-support) follows this shape.
import { z } from "zod";

const KEEP = 12;
const recent = new Map<string, Call[]>();               // per account, in-process
type Call = { tool: string; at: string; ms: number; ok: boolean; error?: string };

export function recordCall(userId: string, c: Call) {   // call from your tool wrapper's finally
  const l = recent.get(userId) ?? []; l.unshift(c); l.length = Math.min(l.length, KEEP); recent.set(userId, l);
}

export function withSupport(server: McpServer, opts: { apiKey: string; rules: Rule[]; user: () => Account }) {
  server.registerTool("support_ask", { description: "Help with <product> itself…", inputSchema: { question: z.string() } },
    async ({ question }) => text(answerFromRules(question, opts.rules, recent.get(opts.user().id) ?? [])));

  server.registerTool("support_open_ticket", { description: "Open a ticket with the <product> team…",
    inputSchema: { subject: z.string(), message: z.string(), priority: z.enum(["LOW","MEDIUM","HIGH","URGENT"]).default("MEDIUM"), category: z.enum(["bug","question","billing","account","feature"]).default("question") } },
    async (input) => {
      const u = opts.user(); const ctx = { account: u, client: {…}, recentCalls: recent.get(u.id) ?? [] };
      const res = await fetch("https://canhelpto.com/api/v1/tickets", { method: "POST",
        headers: { "X-API-Key": opts.apiKey, "X-Service-Mode": "true", "Content-Type": "application/json" },
        body: JSON.stringify({ ...input, message: input.message + "\n\n" + renderContext(ctx), customerName: u.name, customerEmail: u.email, tags: ["mcp", input.category], page: "mcp://<product>", metadata: { mcp: ctx } }) });
      const { ticket } = await res.json();
      return text(`Ticket ${ticket.id} is with the team; read the answer with support_ticket_status.`);
    });

  server.registerTool("support_ticket_status", { description: "Status and replies on a ticket…", inputSchema: { ticketId: z.string().optional() } },
    async ({ ticketId }) => { /* GET the ticket without service mode; refuse unless metadata.mcp.account.userId === user.id or the email matches */ });
}

Your team's Claude: the inbox as an MCP server

The other side of the desk. Your support team connects Claude, Cursor or any MCP client to https://canhelpto.com/api/mcp with the workspace service key as a bearer token, and works the inbox from there: list and read tickets (the connector context included), reply to the customer, add internal notes, set the status, search the knowledge base. A reply reaches the customer by email and through their own connector.

// Claude Code
claude mcp add --transport http canhelpto https://canhelpto.com/api/mcp \
  --header "Authorization: Bearer chs_…"

// Cursor / VS Code (mcp.json)
{ "mcpServers": { "canhelpto": { "url": "https://canhelpto.com/api/mcp",
    "headers": { "Authorization": "Bearer chs_…" } } } }

// Tools: list_tickets · get_ticket · reply_to_ticket · add_internal_note · set_ticket_status · search_knowledge

Two keys per workspace: the public API key is the widget id printed in your pages and opens only what the widget can do; the service key (Dashboard, Settings, Service key) is for server-side callers and the team connector. Once a service key exists, the public key no longer opens service mode. Registry manifest: server.json.

Rules for a good ticket

Questions: [email protected].