Skip to content
146 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1/**
2 * A small client for MCP servers over Streamable HTTP
3 * (docs.g1t.sh/guides/agent-abilities/, "MCP servers"): lists a server's
4 * tools when an owner adds it, and calls one when an agent's ability
5 * allows. JSON-RPC, one request per call, a fresh session each time; an
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.6 * answer may come back as JSON or as a server-sent event stream. A server
7 * that needs a sign-in gets the headers its connector keeps (connectors.ts):
8 * a key, or an OAuth bearer token. Pure apart from `fetch`, so it is tested
9 * with a fake.
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.10 */
11import type { McpTool } from "@g1t/contracts";
12
13import { MAX_MCP_TOOLS } from "../../../packages/contracts/src/abilities.ts";
14
15const PROTOCOL = "2025-06-18";
16/** How long one request to a server may take. */
17const TIMEOUT_MS = 15_000;
18/** The most of a tool's answer an agent is given. */
19const MAX_TEXT = 40_000;
20
21export type Fetch = (url: string, init: RequestInit) => Promise<Response>;
22
23type Rpc = { id?: number | string | null; result?: unknown; error?: { code?: number; message?: string } };
24
25/** One JSON-RPC exchange with the server; the body of the matching answer, or why there is none. */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.26async function exchange(fetchFn: Fetch, url: string, session: string | null, body: Record<string, unknown>, expectAnswer: boolean, headers: Record<string, string> = {}): Promise<{ ok: true; answer: Rpc | null; session: string | null } | { ok: false; message: string }> {
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.27 let response: Response;
28 try {
29 response = await fetchFn(url, {
30 method: "POST",
31 headers: {
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.32 ...headers,
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.33 "content-type": "application/json",
34 accept: "application/json, text/event-stream",
35 "mcp-protocol-version": PROTOCOL,
36 ...(session ? { "mcp-session-id": session } : {}),
37 },
38 body: JSON.stringify({ jsonrpc: "2.0", ...body }),
39 signal: AbortSignal.timeout(TIMEOUT_MS),
40 redirect: "error",
41 });
42 } catch (error) {
43 return { ok: false, message: `The server couldn't be reached (${String(error).slice(0, 120)}).` };
44 }
45 const next = response.headers.get("mcp-session-id") ?? session;
46 if (!expectAnswer) return { ok: true, answer: null, session: next };
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.47 if (response.status === 401 || response.status === 403) return { ok: false, message: `The server refused the sign-in (${response.status}). Sign in again, or check its key.` };
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.48 if (!response.ok) return { ok: false, message: `The server answered ${response.status}.` };
49 const type = (response.headers.get("content-type") ?? "").toLowerCase();
50 let text: string;
51 try {
52 text = await response.text();
53 } catch {
54 return { ok: false, message: "The server's answer couldn't be read." };
55 }
56 const wanted = body.id;
57 const candidates: Rpc[] = [];
58 if (type.includes("text/event-stream")) {
59 for (const event of text.split(/\n\n+/)) {
60 const data = event
61 .split("\n")
62 .filter((line) => line.startsWith("data:"))
63 .map((line) => line.slice(5).trim())
64 .join("\n");
65 if (!data) continue;
66 try {
67 const parsed = JSON.parse(data) as Rpc | Rpc[];
68 candidates.push(...(Array.isArray(parsed) ? parsed : [parsed]));
69 } catch {
70 // Not JSON: a keep-alive or a comment.
71 }
72 }
73 } else {
74 try {
75 const parsed = JSON.parse(text) as Rpc | Rpc[];
76 candidates.push(...(Array.isArray(parsed) ? parsed : [parsed]));
77 } catch {
78 return { ok: false, message: "The server didn't answer with JSON-RPC." };
79 }
80 }
81 const answer = candidates.find((c) => c && typeof c === "object" && c.id === wanted) ?? null;
82 if (!answer) return { ok: false, message: "The server didn't answer the request." };
83 return { ok: true, answer, session: next };
84}
85
86/** Opens a session: initialize, then the initialized notification. The session id, if the server gave one. */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.87async function handshake(fetchFn: Fetch, url: string, headers: Record<string, string>): Promise<{ ok: true; session: string | null } | { ok: false; message: string }> {
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.88 const opened = await exchange(
89 fetchFn,
90 url,
91 null,
92 { id: 1, method: "initialize", params: { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: { name: "g1t-agents", version: "1" } } },
93 true,
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.94 headers,
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.95 );
96 if (!opened.ok) return opened;
97 if (opened.answer?.error) return { ok: false, message: `The server refused to start: ${opened.answer.error.message ?? "no reason given"}.` };
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.98 const told = await exchange(fetchFn, url, opened.session, { method: "notifications/initialized" }, false, headers);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.99 return { ok: true, session: told.ok ? told.session : opened.session };
100}
101
102/** A tool as the server lists it, as kept: its kind from `readOnlyHint`, a write when the server doesn't say. */
103function toolOf(raw: unknown): McpTool | null {
104 if (!raw || typeof raw !== "object") return null;
105 const t = raw as { name?: unknown; description?: unknown; inputSchema?: unknown; annotations?: { readOnlyHint?: unknown } };
106 if (typeof t.name !== "string" || !/^[A-Za-z0-9_.-]{1,64}$/.test(t.name)) return null;
107 const schema = t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? (t.inputSchema as Record<string, unknown>) : { type: "object", properties: {} };
108 return {
109 name: t.name,
110 description: typeof t.description === "string" ? t.description.trim().slice(0, 500) : "",
111 kind: t.annotations?.readOnlyHint === true ? "read" : "write",
112 input_schema: JSON.stringify(schema).length > 20_000 ? { type: "object", properties: {} } : schema,
113 };
114}
115
116/** The server's tools, at most `MAX_MCP_TOOLS`, or why they couldn't be listed. */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.117export async function listMcpTools(url: string, fetchFn: Fetch = (u, init) => fetch(u, init), headers: Record<string, string> = {}): Promise<{ ok: true; tools: McpTool[] } | { ok: false; message: string }> {
118 const opened = await handshake(fetchFn, url, headers);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.119 if (!opened.ok) return opened;
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.120 const listed = await exchange(fetchFn, url, opened.session, { id: 2, method: "tools/list", params: {} }, true, headers);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.121 if (!listed.ok) return listed;
122 if (listed.answer?.error) return { ok: false, message: `The server couldn't list its tools: ${listed.answer.error.message ?? "no reason given"}.` };
123 const raw = (listed.answer?.result as { tools?: unknown } | undefined)?.tools;
124 if (!Array.isArray(raw)) return { ok: false, message: "The server listed no tools." };
125 const tools = raw.map(toolOf).filter((tool): tool is McpTool => tool !== null);
126 const names = new Set<string>();
127 return { ok: true, tools: tools.filter((tool) => (names.has(tool.name) ? false : (names.add(tool.name), true))).slice(0, MAX_MCP_TOOLS) };
128}
129
130/** Calls one tool; the text it returned (its text parts joined), or why it didn't work. */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.131export async function callMcpTool(url: string, name: string, args: Record<string, unknown>, fetchFn: Fetch = (u, init) => fetch(u, init), headers: Record<string, string> = {}): Promise<{ ok: true; text: string } | { ok: false; message: string }> {
132 const opened = await handshake(fetchFn, url, headers);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.133 if (!opened.ok) return opened;
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.134 const called = await exchange(fetchFn, url, opened.session, { id: 3, method: "tools/call", params: { name, arguments: args } }, true, headers);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.135 if (!called.ok) return called;
136 if (called.answer?.error) return { ok: false, message: called.answer.error.message ?? "the server refused" };
137 const result = called.answer?.result as { content?: unknown; isError?: unknown; structuredContent?: unknown } | undefined;
138 const parts = Array.isArray(result?.content) ? (result!.content as { type?: string; text?: string }[]) : [];
139 let text = parts
140 .map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : part?.type ? `[${part.type} content]` : ""))
141 .filter(Boolean)
142 .join("\n");
143 if (!text && result?.structuredContent !== undefined) text = JSON.stringify(result.structuredContent).slice(0, MAX_TEXT);
144 if (result?.isError === true) return { ok: false, message: text.slice(0, 500) || "the tool reported an error" };
145 return { ok: true, text: (text || "(no content)").slice(0, MAX_TEXT) };
146}

This file's history is long; its oldest lines are credited to the oldest commit read.