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
POST /api/v1/ticketsopens a ticket (201).GET /api/v1/tickets/{id}reads it with public replies, metadata and page.GET /api/v1/tickets?email=lists a customer's tickets.POST /api/v1/tickets/{id}/replieslets the customer answer (email must match).- Auth:
X-API-Key. Servers acting for a customer addX-Service-Mode: trueon writes and leave it out on reads that go back to the customer.
Step by step
- 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. 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. 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. 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. 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_knowledgeTwo 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
- The customer's words first, the context under them. The inbox shows both.
- Never repeat the context in the message the agent writes; the server attaches it.
- Give the customer's email when you have it: the reply goes there as well as through the tool.
- Show a customer only their own ticket. The workspace key reads every ticket; the check is yours.
- Three tools at most. Every extra tool costs the agent context on every call.
Questions: [email protected].