@anuma/sdk 6.26.2 → 6.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,6 +112,7 @@ __export(index_exports, {
112
112
  createNotionGetTeamsTool: () => createNotionGetTeamsTool,
113
113
  createNotionGetUsersTool: () => createNotionGetUsersTool,
114
114
  createNotionMovePagesTool: () => createNotionMovePagesTool,
115
+ createNotionProxyTools: () => createNotionProxyTools,
115
116
  createNotionSearchTool: () => createNotionSearchTool,
116
117
  createNotionTools: () => createNotionTools,
117
118
  createNotionUpdateDataSourceTool: () => createNotionUpdateDataSourceTool,
@@ -1728,1306 +1729,1291 @@ function createDriveTools(getAccessToken, requestDriveAccess) {
1728
1729
  ];
1729
1730
  }
1730
1731
 
1731
- // src/tools/notion.ts
1732
- var MCP_HTTP_ENDPOINT = "https://mcp.notion.com/mcp";
1733
- var MCP_SSE_ENDPOINT = "https://mcp.notion.com/sse";
1734
- var MAX_CONTENT_LENGTH2 = 5e4;
1735
- function truncateToolResult(result) {
1736
- if (result === void 0 || result === null) return result;
1737
- const stringified = typeof result === "string" ? result : JSON.stringify(result);
1738
- if (stringified.length <= MAX_CONTENT_LENGTH2) {
1739
- return result;
1732
+ // src/lib/connectors/agentres/errors.ts
1733
+ var AgentresError = class extends Error {
1734
+ constructor(status, code, message, retryable, nextStep) {
1735
+ super(message);
1736
+ this.name = "AgentresError";
1737
+ this.status = status;
1738
+ this.code = code;
1739
+ this.retryable = retryable;
1740
+ this.nextStep = nextStep;
1740
1741
  }
1741
- return stringified.slice(0, MAX_CONTENT_LENGTH2) + `
1742
-
1743
- ... (content truncated, showing first ${MAX_CONTENT_LENGTH2} characters of ${stringified.length})`;
1742
+ };
1743
+ var SiwxUnsupportedError = class extends Error {
1744
+ constructor(message) {
1745
+ super(message);
1746
+ this.name = "SiwxUnsupportedError";
1747
+ }
1748
+ };
1749
+ var AgentresPathError = class extends Error {
1750
+ constructor(message) {
1751
+ super(message);
1752
+ this.name = "AgentresPathError";
1753
+ }
1754
+ };
1755
+ var SiwxChallengeError = class extends Error {
1756
+ constructor(message, options) {
1757
+ super(message);
1758
+ this.name = "SiwxChallengeError";
1759
+ this.cause = options?.cause;
1760
+ }
1761
+ };
1762
+ function parseAgentresError(status, body) {
1763
+ const stated = statedError(body);
1764
+ if (!stated) {
1765
+ return new AgentresError(
1766
+ status,
1767
+ `HTTP_${status}`,
1768
+ body.trim() || `HTTP ${status}`,
1769
+ status >= 500
1770
+ );
1771
+ }
1772
+ const code = typeof stated.code === "string" ? stated.code : `HTTP_${status}`;
1773
+ const message = typeof stated.message === "string" ? stated.message : `HTTP ${status}`;
1774
+ const retryable = typeof stated.retryable === "boolean" ? stated.retryable : status >= 500;
1775
+ const nextStep = typeof stated.next_step === "string" ? stated.next_step : void 0;
1776
+ return new AgentresError(status, code, message, retryable, nextStep);
1744
1777
  }
1745
- var requestIdCounter = Math.floor(Math.random() * 1e6);
1746
- var sessionCache = /* @__PURE__ */ new Map();
1747
- function generateRequestId() {
1748
- return ++requestIdCounter;
1778
+ function statedError(body) {
1779
+ let parsed;
1780
+ try {
1781
+ parsed = JSON.parse(body);
1782
+ } catch {
1783
+ return null;
1784
+ }
1785
+ const envelope = parsed;
1786
+ const stated = typeof envelope?.error === "object" && envelope.error !== null ? envelope.error : null;
1787
+ if (!stated) {
1788
+ return null;
1789
+ }
1790
+ const fields = stated;
1791
+ if (typeof fields.code !== "string" && typeof fields.message !== "string") {
1792
+ return null;
1793
+ }
1794
+ return fields;
1749
1795
  }
1750
- function parseSSEResponse(text) {
1751
- const events = text.split(/\n\n+/);
1752
- let lastEventData = "";
1753
- for (const event of events) {
1754
- const dataLines = [];
1755
- for (const line of event.split("\n")) {
1756
- if (line.startsWith("data: ")) {
1757
- dataLines.push(line.slice(6));
1758
- } else if (line.startsWith("data:")) {
1759
- dataLines.push(line.slice(5));
1760
- }
1761
- }
1762
- if (dataLines.length > 0) {
1763
- lastEventData = dataLines.join("\n");
1764
- }
1796
+
1797
+ // src/lib/processors/encoding.ts
1798
+ var B64_ENCODE_CHUNK = 32768 * 3;
1799
+ function uint8ArrayToBase64(data) {
1800
+ if (typeof Buffer !== "undefined") {
1801
+ return Buffer.from(data).toString("base64");
1765
1802
  }
1766
- if (!lastEventData) {
1767
- throw new Error("No data found in SSE response");
1803
+ let base64 = "";
1804
+ for (let i = 0; i < data.length; i += B64_ENCODE_CHUNK) {
1805
+ const slice = data.subarray(i, i + B64_ENCODE_CHUNK);
1806
+ let binary = "";
1807
+ for (let j = 0; j < slice.length; j++) {
1808
+ binary += String.fromCharCode(slice[j]);
1809
+ }
1810
+ base64 += btoa(binary);
1768
1811
  }
1769
- return JSON.parse(lastEventData);
1812
+ return base64;
1770
1813
  }
1771
- async function parseResponseBody(response) {
1772
- const contentType = response.headers.get("Content-Type") || "";
1773
- const text = await response.text();
1774
- if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
1775
- return parseSSEResponse(text);
1814
+ function base64ToUint8Array(base64) {
1815
+ if (typeof Buffer !== "undefined") {
1816
+ const buf = Buffer.from(base64, "base64");
1817
+ return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
1776
1818
  }
1777
- return JSON.parse(text);
1819
+ const binary = atob(base64);
1820
+ const bytes = new Uint8Array(binary.length);
1821
+ for (let i = 0; i < binary.length; i++) {
1822
+ bytes[i] = binary.charCodeAt(i);
1823
+ }
1824
+ return bytes;
1778
1825
  }
1779
- async function initializeMCPSession(accessToken) {
1780
- const requestId = generateRequestId();
1781
- const initRequest = {
1782
- jsonrpc: "2.0",
1783
- id: requestId,
1784
- method: "initialize",
1785
- params: {
1786
- protocolVersion: "2024-11-05",
1787
- capabilities: {},
1788
- clientInfo: {
1789
- name: "Anuma",
1790
- version: "1.0.0"
1791
- }
1792
- }
1826
+
1827
+ // src/lib/connectors/agentres/siwx.ts
1828
+ var PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
1829
+ var SIWX_HEADER = "SIGN-IN-WITH-X";
1830
+ var SIWX_EXTENSION = "sign-in-with-x";
1831
+ var SOLANA_NAMESPACE = "solana:";
1832
+ var ED25519_SIGNATURE_BYTES = 64;
1833
+ function parseChallenge(paymentRequiredHeader, expectedOrigin) {
1834
+ const decoded = decodeHeader(paymentRequiredHeader);
1835
+ const extensions = asRecord(decoded.extensions);
1836
+ const extension = extensions ? asRecord(extensions[SIWX_EXTENSION]) : null;
1837
+ if (!extension) {
1838
+ throw new SiwxUnsupportedError(
1839
+ "the 402 carries no sign-in-with-x extension, so this endpoint is paid rather than identity-only"
1840
+ );
1841
+ }
1842
+ const info = asRecord(extension.info);
1843
+ if (!info) {
1844
+ throw new SiwxChallengeError("the sign-in-with-x extension has no info block");
1845
+ }
1846
+ const chain = solanaChain(extension.supportedChains);
1847
+ const challenge = {
1848
+ domain: requireString(info, "domain"),
1849
+ uri: requireString(info, "uri"),
1850
+ version: requireString(info, "version"),
1851
+ statement: requireString(info, "statement"),
1852
+ nonce: requireString(info, "nonce"),
1853
+ issuedAt: requireString(info, "issuedAt"),
1854
+ resources: requireResources(info),
1855
+ chainId: chain.chainId,
1856
+ signingType: chain.signingType
1793
1857
  };
1794
- const response = await fetch(MCP_HTTP_ENDPOINT, {
1795
- method: "POST",
1796
- headers: {
1797
- "Content-Type": "application/json",
1798
- Accept: "application/json, text/event-stream",
1799
- Authorization: `Bearer ${accessToken}`
1800
- },
1801
- body: JSON.stringify(initRequest)
1802
- });
1803
- if (!response.ok) {
1804
- const errorBody = await response.text().catch(() => "");
1805
- throw new Error(
1806
- `MCP initialization failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
1858
+ assertScopedTo(challenge, expectedOrigin);
1859
+ return challenge;
1860
+ }
1861
+ function assertScopedTo(challenge, expectedOrigin) {
1862
+ const expected = parseUrl(expectedOrigin, `the agentres base URL "${expectedOrigin}"`);
1863
+ if (challenge.domain !== expected.host) {
1864
+ throw new SiwxChallengeError(
1865
+ `the challenge is scoped to "${challenge.domain}" but this client talks to "${expected.host}" \u2014 refusing to sign it`
1807
1866
  );
1808
1867
  }
1809
- const sessionId = response.headers.get("Mcp-Session-Id");
1810
- if (!sessionId) {
1811
- throw new Error("No Mcp-Session-Id returned from initialization");
1868
+ const uri = parseUrl(challenge.uri, `the sign-in-with-x uri "${challenge.uri}"`);
1869
+ if (uri.origin !== expected.origin) {
1870
+ throw new SiwxChallengeError(
1871
+ `the challenge addresses "${uri.origin}" but this client talks to "${expected.origin}" \u2014 refusing to sign it`
1872
+ );
1812
1873
  }
1813
- const jsonRpcResponse = await parseResponseBody(response);
1814
- if (jsonRpcResponse.error) {
1815
- const err = jsonRpcResponse.error;
1816
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1817
- throw new Error(`MCP initialization error: ${errMsg}`);
1874
+ }
1875
+ function parseUrl(value, description) {
1876
+ try {
1877
+ return new URL(value);
1878
+ } catch (cause) {
1879
+ throw new SiwxChallengeError(`${description} is not a URL`, { cause });
1818
1880
  }
1819
- await fetch(MCP_HTTP_ENDPOINT, {
1820
- method: "POST",
1821
- headers: {
1822
- "Content-Type": "application/json",
1823
- Authorization: `Bearer ${accessToken}`,
1824
- "Mcp-Session-Id": sessionId
1825
- },
1826
- body: JSON.stringify({
1827
- jsonrpc: "2.0",
1828
- method: "notifications/initialized"
1829
- })
1830
- });
1831
- sessionCache.set(accessToken, sessionId);
1832
- return sessionId;
1833
1881
  }
1834
- async function ensureMCPSession(accessToken) {
1835
- const cached = sessionCache.get(accessToken);
1836
- if (cached) {
1837
- return cached;
1882
+ function buildMessage(challenge, address) {
1883
+ if (!challenge.chainId.startsWith(SOLANA_NAMESPACE)) {
1884
+ throw new SiwxChallengeError(
1885
+ `expected a solana: chain id for the message, got "${challenge.chainId}"`
1886
+ );
1838
1887
  }
1839
- return initializeMCPSession(accessToken);
1888
+ const reference = challenge.chainId.slice(SOLANA_NAMESPACE.length);
1889
+ const resources = challenge.resources.map((resource) => `- ${resource}`).join("\n");
1890
+ return `${challenge.domain} wants you to sign in with your Solana account:
1891
+ ${address}
1892
+
1893
+ ${challenge.statement}
1894
+
1895
+ URI: ${challenge.uri}
1896
+ Version: ${challenge.version}
1897
+ Chain ID: ${reference}
1898
+ Nonce: ${challenge.nonce}
1899
+ Issued At: ${challenge.issuedAt}
1900
+ Resources:
1901
+ ${resources}`;
1840
1902
  }
1841
- async function callMCPTool(accessToken, toolName, args) {
1842
- const sessionId = await ensureMCPSession(accessToken);
1843
- const requestId = generateRequestId();
1844
- const jsonRpcRequest = {
1845
- jsonrpc: "2.0",
1846
- id: requestId,
1847
- method: "tools/call",
1848
- params: {
1849
- name: toolName,
1850
- arguments: args
1851
- }
1903
+ function buildPayload(challenge, address, signature) {
1904
+ if (signature.length !== ED25519_SIGNATURE_BYTES) {
1905
+ throw new SiwxChallengeError(
1906
+ `expected a ${ED25519_SIGNATURE_BYTES}-byte ed25519 signature, got ${signature.length} bytes`
1907
+ );
1908
+ }
1909
+ const payload = {
1910
+ domain: challenge.domain,
1911
+ address,
1912
+ statement: challenge.statement,
1913
+ uri: challenge.uri,
1914
+ version: challenge.version,
1915
+ // Full CAIP-2 here, unlike the bare reference the message carries.
1916
+ chainId: challenge.chainId,
1917
+ type: challenge.signingType,
1918
+ nonce: challenge.nonce,
1919
+ issuedAt: challenge.issuedAt,
1920
+ resources: challenge.resources,
1921
+ signature: base58Encode(signature)
1852
1922
  };
1853
- const response = await fetch(MCP_HTTP_ENDPOINT, {
1854
- method: "POST",
1855
- headers: {
1856
- "Content-Type": "application/json",
1857
- Accept: "application/json, text/event-stream",
1858
- Authorization: `Bearer ${accessToken}`,
1859
- "Mcp-Session-Id": sessionId
1860
- },
1861
- body: JSON.stringify(jsonRpcRequest)
1862
- });
1863
- if (!response.ok) {
1864
- if (response.status === 400 || response.status === 401) {
1865
- sessionCache.delete(accessToken);
1866
- const newSessionId = await initializeMCPSession(accessToken);
1867
- const retryResponse = await fetch(MCP_HTTP_ENDPOINT, {
1868
- method: "POST",
1869
- headers: {
1870
- "Content-Type": "application/json",
1871
- Accept: "application/json, text/event-stream",
1872
- Authorization: `Bearer ${accessToken}`,
1873
- "Mcp-Session-Id": newSessionId
1874
- },
1875
- body: JSON.stringify(jsonRpcRequest)
1876
- });
1877
- if (!retryResponse.ok) {
1878
- const errorBody2 = await retryResponse.text().catch(() => "");
1879
- throw new Error(
1880
- `MCP request failed: ${retryResponse.status} ${retryResponse.statusText}${errorBody2 ? ` - ${errorBody2}` : ""}`
1881
- );
1882
- }
1883
- const retryJsonRpcResponse = await parseResponseBody(retryResponse);
1884
- if (retryJsonRpcResponse.error) {
1885
- const err = retryJsonRpcResponse.error;
1886
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1887
- throw new Error(`MCP tool error: ${errMsg}`);
1888
- }
1889
- return truncateToolResult(retryJsonRpcResponse.result);
1923
+ return uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload)));
1924
+ }
1925
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1926
+ function base58Encode(bytes) {
1927
+ let zeros = 0;
1928
+ while (zeros < bytes.length && bytes[zeros] === 0) {
1929
+ zeros++;
1930
+ }
1931
+ const digits = [];
1932
+ for (let i = zeros; i < bytes.length; i++) {
1933
+ let carry = bytes[i];
1934
+ for (let j = 0; j < digits.length; j++) {
1935
+ carry += digits[j] * 256;
1936
+ digits[j] = carry % 58;
1937
+ carry = Math.floor(carry / 58);
1938
+ }
1939
+ while (carry > 0) {
1940
+ digits.push(carry % 58);
1941
+ carry = Math.floor(carry / 58);
1890
1942
  }
1891
- const errorBody = await response.text().catch(() => "");
1892
- throw new Error(
1893
- `MCP request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
1894
- );
1895
1943
  }
1896
- const jsonRpcResponse = await parseResponseBody(response);
1897
- if (jsonRpcResponse.error) {
1898
- const err = jsonRpcResponse.error;
1899
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1900
- throw new Error(`MCP tool error: ${errMsg}`);
1944
+ let encoded = "1".repeat(zeros);
1945
+ for (let i = digits.length - 1; i >= 0; i--) {
1946
+ encoded += BASE58_ALPHABET[digits[i]];
1901
1947
  }
1902
- return truncateToolResult(jsonRpcResponse.result);
1948
+ return encoded;
1903
1949
  }
1904
- function createNotionSearchTool(getAccessToken, requestNotionAccess) {
1950
+ function decodeHeader(paymentRequiredHeader) {
1951
+ let parsed;
1952
+ try {
1953
+ parsed = JSON.parse(new TextDecoder().decode(base64ToUint8Array(paymentRequiredHeader)));
1954
+ } catch (cause) {
1955
+ throw new SiwxChallengeError("the PAYMENT-REQUIRED header is not base64-encoded JSON", {
1956
+ cause
1957
+ });
1958
+ }
1959
+ const decoded = asRecord(parsed);
1960
+ if (!decoded) {
1961
+ throw new SiwxChallengeError("the PAYMENT-REQUIRED header did not decode to an object");
1962
+ }
1963
+ return decoded;
1964
+ }
1965
+ function solanaChain(supportedChains) {
1966
+ if (!Array.isArray(supportedChains)) {
1967
+ throw new SiwxChallengeError("the sign-in-with-x extension lists no supportedChains");
1968
+ }
1969
+ for (const entry of supportedChains) {
1970
+ const chain = asRecord(entry);
1971
+ const chainId = chain?.chainId;
1972
+ if (typeof chainId !== "string" || !chainId.startsWith(SOLANA_NAMESPACE)) {
1973
+ continue;
1974
+ }
1975
+ const signingType = chain?.type;
1976
+ if (typeof signingType !== "string" || signingType === "") {
1977
+ throw new SiwxChallengeError(`the ${chainId} entry declares no signing type`);
1978
+ }
1979
+ return { chainId, signingType };
1980
+ }
1981
+ throw new SiwxChallengeError("the sign-in-with-x extension offers no solana: chain");
1982
+ }
1983
+ function asRecord(value) {
1984
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1985
+ }
1986
+ function requireString(info, key) {
1987
+ const value = info[key];
1988
+ if (typeof value !== "string" || value === "") {
1989
+ throw new SiwxChallengeError(`the sign-in-with-x info block has no ${key}`);
1990
+ }
1991
+ return value;
1992
+ }
1993
+ function requireResources(info) {
1994
+ const resources = info.resources;
1995
+ if (!Array.isArray(resources) || resources.length === 0) {
1996
+ throw new SiwxChallengeError("the sign-in-with-x info block has no resources");
1997
+ }
1998
+ if (!resources.every((resource) => typeof resource === "string" && resource !== "")) {
1999
+ throw new SiwxChallengeError("the sign-in-with-x resources are not all strings");
2000
+ }
2001
+ return resources;
2002
+ }
2003
+
2004
+ // src/lib/connectors/agentres/client.ts
2005
+ var DEFAULT_BASE_URL = "https://agentres.dev";
2006
+ function createAgentresClient(options) {
2007
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
2008
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
2009
+ const { address, signMessage } = options;
2010
+ async function withSiwx(request) {
2011
+ const url = resolveUrl(baseUrl, request.path);
2012
+ const init = requestInit(request);
2013
+ const challenged = await fetchImpl(url, init);
2014
+ if (challenged.status !== 402) {
2015
+ return readBody(challenged);
2016
+ }
2017
+ await challenged.body?.cancel().catch(() => {
2018
+ });
2019
+ const header = challenged.headers.get(PAYMENT_REQUIRED_HEADER);
2020
+ if (!header) {
2021
+ throw new SiwxChallengeError(
2022
+ `the 402 from ${request.path} carried no ${PAYMENT_REQUIRED_HEADER} header`
2023
+ );
2024
+ }
2025
+ const challenge = parseChallenge(header, baseUrl);
2026
+ const signature = await signMessage(new TextEncoder().encode(buildMessage(challenge, address)));
2027
+ const authorized = await fetchImpl(url, {
2028
+ ...init,
2029
+ headers: { ...init.headers, [SIWX_HEADER]: buildPayload(challenge, address, signature) }
2030
+ });
2031
+ return readBody(authorized);
2032
+ }
2033
+ async function linkResy(body) {
2034
+ const response = await withSiwx({ method: "POST", path: "/api/link-resy", body });
2035
+ return { step: response.step, message: response.message, resyUserId: response.resy_user_id };
2036
+ }
1905
2037
  return {
1906
- type: "function",
1907
- function: {
1908
- name: "notion-search",
1909
- description: "Semantic search over Notion workspace and connected sources (Slack, Google Drive, Github, Jira, etc). Can also search for users by name or email.",
1910
- arguments: {
1911
- type: "object",
1912
- properties: {
1913
- query: {
1914
- type: "string",
1915
- description: "Semantic search query over workspace and connected sources. For user search, a substring to match against name or email."
1916
- },
1917
- query_type: {
1918
- type: "string",
1919
- enum: ["internal", "user"],
1920
- description: "Type of search: 'internal' for workspace/content search, 'user' for user search"
1921
- },
1922
- content_search_mode: {
1923
- type: "string",
1924
- enum: ["workspace_search", "ai_search"],
1925
- description: "Override auto-selection of AI search vs workspace search"
1926
- },
1927
- data_source_url: {
1928
- type: "string",
1929
- description: "URL of a data source to search within"
1930
- },
1931
- page_url: {
1932
- type: "string",
1933
- description: "URL or ID of a page to search within"
1934
- },
1935
- teamspace_id: {
1936
- type: "string",
1937
- description: "ID of a teamspace to restrict search results to"
1938
- },
1939
- filters: {
1940
- type: "object",
1941
- description: "Filters for search results (only for query_type 'internal'). Supports created_date_range {start_date, end_date} and created_by_user_ids array."
1942
- }
1943
- },
1944
- required: ["query"]
1945
- }
2038
+ withSiwx,
2039
+ async ensureAccount(email) {
2040
+ const body = await withSiwx({ method: "POST", path: "/api/account", body: { email } });
2041
+ return {
2042
+ accountId: body.account_id,
2043
+ email: body.email,
2044
+ credentialKind: body.credential_kind,
2045
+ walletLinked: body.wallet_linked
2046
+ };
1946
2047
  },
1947
- executor: async (args) => {
1948
- let token = getAccessToken();
1949
- if (!token) {
1950
- token = await requestNotionAccess();
1951
- }
1952
- try {
1953
- return await callMCPTool(token, "notion-search", args);
1954
- } catch (error) {
1955
- return `Error searching Notion: ${error instanceof Error ? error.message : String(error)}`;
1956
- }
2048
+ requestCode(email) {
2049
+ return linkResy({ em_address: email });
2050
+ },
2051
+ verifyCode(email, code) {
2052
+ return linkResy({ em_address: email, code });
2053
+ },
2054
+ async getLinkStatus() {
2055
+ const body = await withSiwx({ method: "GET", path: "/api/me" });
2056
+ return {
2057
+ accountId: body.account_id,
2058
+ email: body.email,
2059
+ resyLinked: body.resy_linked,
2060
+ walletAddress: body.wallet_address,
2061
+ resyLinkedAt: body.resy_linked_at ?? null
2062
+ };
1957
2063
  }
1958
2064
  };
1959
2065
  }
1960
- function createNotionFetchTool(getAccessToken, requestNotionAccess) {
1961
- return {
1962
- type: "function",
1963
- function: {
1964
- name: "notion-fetch",
1965
- description: "Retrieve a Notion page or database by URL or ID. Pages return enhanced Markdown. Databases return all data sources.",
1966
- arguments: {
1967
- type: "object",
1968
- properties: {
1969
- id: {
1970
- type: "string",
1971
- description: "The ID or URL of the Notion page/database. Supports notion.so URLs, Notion Sites URLs (*.notion.site), and raw UUIDs."
1972
- },
1973
- include_transcript: {
1974
- type: "boolean",
1975
- description: "Include transcript if available"
1976
- },
1977
- include_discussions: {
1978
- type: "boolean",
1979
- description: "Include discussion counts and inline discussion markers"
1980
- }
1981
- },
1982
- required: ["id"]
1983
- }
1984
- },
1985
- executor: async (args) => {
1986
- let token = getAccessToken();
1987
- if (!token) {
1988
- token = await requestNotionAccess();
1989
- }
1990
- try {
1991
- return await callMCPTool(token, "notion-fetch", args);
1992
- } catch (error) {
1993
- return `Error fetching Notion page: ${error instanceof Error ? error.message : String(error)}`;
1994
- }
1995
- }
1996
- };
1997
- }
1998
- function createNotionCreatePagesTool(getAccessToken, requestNotionAccess) {
1999
- return {
2000
- type: "function",
2001
- function: {
2002
- name: "notion-create-pages",
2003
- description: "Create one or more Notion pages. All pages in a single call share the same parent. Properties use simplified format: title is a string, dates use date:{prop}:start format, checkboxes use __YES__/__NO__. Content is Notion-flavored Markdown.",
2004
- arguments: {
2005
- type: "object",
2006
- properties: {
2007
- pages: {
2008
- type: "array",
2009
- description: "Array of page objects to create (max 100)",
2010
- items: {
2011
- type: "object",
2012
- properties: {
2013
- properties: {
2014
- type: "object",
2015
- description: "JSON map of property names to values. Title is a string, numbers are JS numbers, checkboxes use __YES__/__NO__, dates use date:{prop}:start/end/is_datetime format."
2016
- },
2017
- content: {
2018
- type: "string",
2019
- description: "Page content in Notion-flavored Markdown format"
2020
- }
2021
- },
2022
- required: ["properties", "content"]
2023
- }
2024
- },
2025
- parent: {
2026
- type: "object",
2027
- description: "Parent for all pages. Use {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, or {type:'data_source_id', data_source_id:'...'}. Do NOT pass this field to create standalone private pages."
2028
- }
2029
- },
2030
- required: ["pages"]
2031
- }
2032
- },
2033
- executor: async (args) => {
2034
- let token = getAccessToken();
2035
- if (!token) {
2036
- token = await requestNotionAccess();
2037
- }
2038
- try {
2039
- return await callMCPTool(token, "notion-create-pages", args);
2040
- } catch (error) {
2041
- return `Error creating Notion page: ${error instanceof Error ? error.message : String(error)}`;
2042
- }
2043
- }
2044
- };
2045
- }
2046
- function createNotionUpdatePageTool(getAccessToken, requestNotionAccess) {
2047
- return {
2048
- type: "function",
2049
- function: {
2050
- name: "notion-update-page",
2051
- description: "Update a Notion page's properties or content. Uses command-based operations: 'update_properties' to change properties, 'replace_content' to replace all content, 'replace_content_range' to replace specific text, 'insert_content_after' to insert after text.",
2052
- arguments: {
2053
- type: "object",
2054
- properties: {
2055
- data: {
2056
- type: "object",
2057
- description: "Update data containing page_id and a command",
2058
- properties: {
2059
- page_id: {
2060
- type: "string",
2061
- description: "The ID of the page to update (with or without dashes)"
2062
- },
2063
- command: {
2064
- type: "string",
2065
- enum: [
2066
- "update_properties",
2067
- "replace_content",
2068
- "replace_content_range",
2069
- "insert_content_after"
2070
- ],
2071
- description: "The update command to execute"
2072
- },
2073
- properties: {
2074
- type: "object",
2075
- description: "For update_properties: JSON map of property names to values. Use null to remove a value."
2076
- },
2077
- new_str: {
2078
- type: "string",
2079
- description: "For replace_content/replace_content_range/insert_content_after: the new content string"
2080
- },
2081
- selection_with_ellipsis: {
2082
- type: "string",
2083
- description: "For replace_content_range/insert_content_after: unique start and end snippet (~10 chars each with ellipsis)"
2084
- },
2085
- allow_deleting_content: {
2086
- type: "boolean",
2087
- description: "For replace_content/replace_content_range: allow deletion of child pages/databases"
2088
- }
2089
- },
2090
- required: ["page_id", "command"]
2091
- }
2092
- },
2093
- required: ["data"]
2094
- }
2095
- },
2096
- executor: async (args) => {
2097
- let token = getAccessToken();
2098
- if (!token) {
2099
- token = await requestNotionAccess();
2100
- }
2101
- try {
2102
- return await callMCPTool(token, "notion-update-page", args);
2103
- } catch (error) {
2104
- return `Error updating Notion page: ${error instanceof Error ? error.message : String(error)}`;
2105
- }
2106
- }
2107
- };
2066
+ function resolveUrl(baseUrl, path) {
2067
+ if (!path.startsWith("/") || path.startsWith("//")) {
2068
+ throw new AgentresPathError(
2069
+ `"${path}" is not a path on ${baseUrl} \u2014 it must start with a single "/"`
2070
+ );
2071
+ }
2072
+ const base = new URL(baseUrl);
2073
+ const resolved = new URL(path, base);
2074
+ if (resolved.origin !== base.origin) {
2075
+ throw new AgentresPathError(
2076
+ `"${path}" resolves to ${resolved.origin} but this client talks to ${base.origin} \u2014 refusing to send it`
2077
+ );
2078
+ }
2079
+ return resolved.toString();
2108
2080
  }
2109
- function createNotionMovePagesTool(getAccessToken, requestNotionAccess) {
2110
- return {
2111
- type: "function",
2112
- function: {
2113
- name: "notion-move-pages",
2114
- description: "Move one or more Notion pages or databases to a new parent.",
2115
- arguments: {
2116
- type: "object",
2117
- properties: {
2118
- page_or_database_ids: {
2119
- type: "array",
2120
- description: "Array of page or database IDs to move (UUIDs, max 100)",
2121
- items: { type: "string" }
2122
- },
2123
- new_parent: {
2124
- type: "object",
2125
- description: "New parent: {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, {type:'data_source_id', data_source_id:'...'}, or {type:'workspace'} for private pages."
2126
- }
2127
- },
2128
- required: ["page_or_database_ids", "new_parent"]
2129
- }
2130
- },
2131
- executor: async (args) => {
2132
- let token = getAccessToken();
2133
- if (!token) {
2134
- token = await requestNotionAccess();
2135
- }
2136
- try {
2137
- return await callMCPTool(token, "notion-move-pages", args);
2138
- } catch (error) {
2139
- return `Error moving Notion pages: ${error instanceof Error ? error.message : String(error)}`;
2140
- }
2141
- }
2142
- };
2081
+ function requestInit(request) {
2082
+ const headers = { Accept: "application/json" };
2083
+ if (request.body === void 0) {
2084
+ return { method: request.method, headers };
2085
+ }
2086
+ headers["Content-Type"] = "application/json";
2087
+ return { method: request.method, headers, body: JSON.stringify(request.body) };
2143
2088
  }
2144
- function createNotionDuplicatePageTool(getAccessToken, requestNotionAccess) {
2145
- return {
2146
- type: "function",
2147
- function: {
2148
- name: "notion-duplicate-page",
2149
- description: "Duplicate a Notion page. The duplication completes asynchronously.",
2150
- arguments: {
2151
- type: "object",
2152
- properties: {
2153
- page_id: {
2154
- type: "string",
2155
- description: "The ID of the page to duplicate (UUID with or without dashes)"
2156
- }
2157
- },
2158
- required: ["page_id"]
2159
- }
2160
- },
2161
- executor: async (args) => {
2162
- let token = getAccessToken();
2163
- if (!token) {
2164
- token = await requestNotionAccess();
2165
- }
2166
- try {
2167
- return await callMCPTool(token, "notion-duplicate-page", args);
2168
- } catch (error) {
2169
- return `Error duplicating Notion page: ${error instanceof Error ? error.message : String(error)}`;
2170
- }
2171
- }
2172
- };
2089
+ async function readBody(response) {
2090
+ const body = await response.text();
2091
+ if (!response.ok) {
2092
+ throw parseAgentresError(response.status, body);
2093
+ }
2094
+ try {
2095
+ return JSON.parse(body);
2096
+ } catch {
2097
+ throw new AgentresError(
2098
+ response.status,
2099
+ "INVALID_JSON",
2100
+ `agentres answered ${response.status} with a body that is not JSON`,
2101
+ response.status >= 500
2102
+ );
2103
+ }
2173
2104
  }
2174
- function createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess) {
2175
- return {
2176
- type: "function",
2177
- function: {
2178
- name: "notion-create-database",
2179
- description: "Create a new Notion database with a properties schema. If no title property is provided, 'Name' is auto-added. Supports property types: title, rich_text, number, select, multi_select, date, people, checkbox, url, email, phone_number, formula, relation, rollup, status, unique_id, etc.",
2180
- arguments: {
2181
- type: "object",
2182
- properties: {
2183
- properties: {
2184
- type: "object",
2185
- description: "Property schema for the database. Each key is a property name, value defines the type."
2186
- },
2187
- parent: {
2188
- type: "object",
2189
- description: "Parent page: {type:'page_id', page_id:'...'}. Omit for private workspace-level database."
2190
- },
2191
- title: {
2192
- type: "array",
2193
- description: "Title of the database as rich text array (max 100)",
2194
- items: { type: "object" }
2195
- },
2196
- description: {
2197
- type: "array",
2198
- description: "Description of the database as rich text array (max 100)",
2199
- items: { type: "object" }
2200
- }
2201
- },
2202
- required: ["properties"]
2203
- }
2204
- },
2205
- executor: async (args) => {
2206
- let token = getAccessToken();
2207
- if (!token) {
2208
- token = await requestNotionAccess();
2209
- }
2210
- try {
2211
- return await callMCPTool(token, "notion-create-database", args);
2212
- } catch (error) {
2213
- return `Error creating Notion database: ${error instanceof Error ? error.message : String(error)}`;
2214
- }
2105
+
2106
+ // src/lib/connectors/client.ts
2107
+ var DEFAULT_REFRESH_BEFORE_MS = 3e4;
2108
+ function createConnectorTokenGetter(client, provider, opts = {}) {
2109
+ const refreshBeforeMs = opts.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
2110
+ const now = opts.now ?? Date.now;
2111
+ let cached = null;
2112
+ return async () => {
2113
+ if (cached && cached.expiresAt - refreshBeforeMs > now()) {
2114
+ return cached.accessToken;
2215
2115
  }
2216
- };
2217
- }
2218
- function createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess) {
2219
- return {
2220
- type: "function",
2221
- function: {
2222
- name: "notion-update-data-source",
2223
- description: "Update a Notion data source's title, description, property schema, or other attributes. Use null to remove a property. Provide only 'name' to rename a property.",
2224
- arguments: {
2225
- type: "object",
2226
- properties: {
2227
- data_source_id: {
2228
- type: "string",
2229
- description: "The ID of the data source to update (UUID). Can be a data source ID or database ID."
2230
- },
2231
- title: {
2232
- type: "array",
2233
- description: "New title as rich text array (max 100)",
2234
- items: { type: "object" }
2235
- },
2236
- description: {
2237
- type: "array",
2238
- description: "New description as rich text array (max 100)",
2239
- items: { type: "object" }
2240
- },
2241
- properties: {
2242
- type: "object",
2243
- description: "Property schema updates. Use null to remove, {name:'...'} to rename, or full definition to add/update."
2244
- },
2245
- is_inline: {
2246
- type: "boolean",
2247
- description: "Whether the database is inline"
2248
- },
2249
- in_trash: {
2250
- type: "boolean",
2251
- description: "Trash the data source (requires Notion UI to undo)"
2252
- }
2253
- },
2254
- required: ["data_source_id"]
2255
- }
2256
- },
2257
- executor: async (args) => {
2258
- let token = getAccessToken();
2259
- if (!token) {
2260
- token = await requestNotionAccess();
2261
- }
2262
- try {
2263
- return await callMCPTool(token, "notion-update-data-source", args);
2264
- } catch (error) {
2265
- return `Error updating Notion data source: ${error instanceof Error ? error.message : String(error)}`;
2266
- }
2116
+ let result;
2117
+ try {
2118
+ result = await client.mintConnectorToken(provider);
2119
+ } catch {
2120
+ cached = null;
2121
+ return null;
2267
2122
  }
2268
- };
2269
- }
2270
- function createNotionCreateCommentTool(getAccessToken, requestNotionAccess) {
2271
- return {
2272
- type: "function",
2273
- function: {
2274
- name: "notion-create-comment",
2275
- description: "Add a comment to a Notion page. Supports page-level comments, content-specific comments (using selection_with_ellipsis), or replies to existing discussions (using discussion_id).",
2276
- arguments: {
2277
- type: "object",
2278
- properties: {
2279
- page_id: {
2280
- type: "string",
2281
- description: "The ID of the page to comment on (with or without dashes)"
2282
- },
2283
- rich_text: {
2284
- type: "array",
2285
- description: "Comment content as rich text array (max 100). Objects can be text, mention, or equation.",
2286
- items: {
2287
- type: "object",
2288
- properties: {
2289
- text: {
2290
- type: "object",
2291
- properties: {
2292
- content: { type: "string" }
2293
- },
2294
- required: ["content"]
2295
- }
2296
- },
2297
- required: ["text"]
2298
- }
2299
- },
2300
- discussion_id: {
2301
- type: "string",
2302
- description: "ID or URL of an existing discussion to reply to"
2303
- },
2304
- selection_with_ellipsis: {
2305
- type: "string",
2306
- description: "Unique start and end snippet (~10 chars each with ellipsis) of content to comment on"
2307
- }
2308
- },
2309
- required: ["page_id", "rich_text"]
2310
- }
2311
- },
2312
- executor: async (args) => {
2313
- let token = getAccessToken();
2314
- if (!token) {
2315
- token = await requestNotionAccess();
2316
- }
2317
- try {
2318
- return await callMCPTool(token, "notion-create-comment", args);
2319
- } catch (error) {
2320
- return `Error creating Notion comment: ${error instanceof Error ? error.message : String(error)}`;
2321
- }
2123
+ if (result.ok) {
2124
+ cached = { accessToken: result.accessToken, expiresAt: result.expiresAt };
2125
+ return result.accessToken;
2322
2126
  }
2323
- };
2324
- }
2325
- function createNotionGetCommentsTool(getAccessToken, requestNotionAccess) {
2326
- return {
2327
- type: "function",
2328
- function: {
2329
- name: "notion-get-comments",
2330
- description: "Get comments and discussions from a Notion page. Returns discussions with full comment content.",
2331
- arguments: {
2332
- type: "object",
2333
- properties: {
2334
- page_id: {
2335
- type: "string",
2336
- description: "Identifier for the Notion page"
2337
- },
2338
- include_all_blocks: {
2339
- type: "boolean",
2340
- description: "Include discussions on child blocks (default: false)"
2341
- },
2342
- include_resolved: {
2343
- type: "boolean",
2344
- description: "Include resolved discussions (default: false)"
2345
- },
2346
- discussion_id: {
2347
- type: "string",
2348
- description: "Fetch a specific discussion by ID or URL (e.g., discussion://pageId/blockId/discussionId)"
2349
- }
2350
- },
2351
- required: ["page_id"]
2352
- }
2353
- },
2354
- executor: async (args) => {
2355
- let token = getAccessToken();
2356
- if (!token) {
2357
- token = await requestNotionAccess();
2358
- }
2359
- try {
2360
- return await callMCPTool(token, "notion-get-comments", args);
2361
- } catch (error) {
2362
- return `Error retrieving Notion comments: ${error instanceof Error ? error.message : String(error)}`;
2363
- }
2127
+ cached = null;
2128
+ if (opts.onNotConnected) {
2129
+ return opts.onNotConnected(result.error);
2364
2130
  }
2131
+ return null;
2365
2132
  };
2366
2133
  }
2367
- function createNotionGetUsersTool(getAccessToken, requestNotionAccess) {
2368
- return {
2369
- type: "function",
2370
- function: {
2371
- name: "notion-get-users",
2372
- description: "List users in the Notion workspace. Supports filtering by name/email, pagination, fetching a specific user by ID, or fetching the current bot user (user_id: 'self').",
2373
- arguments: {
2374
- type: "object",
2375
- properties: {
2376
- query: {
2377
- type: "string",
2378
- description: "Search query to filter users by name or email (case-insensitive, max 100 chars)"
2379
- },
2380
- start_cursor: {
2381
- type: "string",
2382
- description: "Cursor for pagination from previous response"
2383
- },
2384
- page_size: {
2385
- type: "integer",
2386
- description: "Number of users per page (1-100, default 100)"
2387
- },
2388
- user_id: {
2389
- type: "string",
2390
- description: "Return only the user matching this ID. Pass 'self' to fetch the current bot user."
2391
- }
2392
- }
2393
- }
2394
- },
2395
- executor: async (args) => {
2396
- let token = getAccessToken();
2397
- if (!token) {
2398
- token = await requestNotionAccess();
2399
- }
2400
- try {
2401
- return await callMCPTool(token, "notion-get-users", args);
2402
- } catch (error) {
2403
- return `Error listing Notion users: ${error instanceof Error ? error.message : String(error)}`;
2134
+
2135
+ // src/tools/notion.ts
2136
+ var MCP_HTTP_ENDPOINT = "https://mcp.notion.com/mcp";
2137
+ var MCP_SSE_ENDPOINT = "https://mcp.notion.com/sse";
2138
+ var MAX_CONTENT_LENGTH2 = 5e4;
2139
+ function truncateToolResult(result) {
2140
+ if (result === void 0 || result === null) return result;
2141
+ const stringified = typeof result === "string" ? result : JSON.stringify(result);
2142
+ if (stringified.length <= MAX_CONTENT_LENGTH2) {
2143
+ return result;
2144
+ }
2145
+ return stringified.slice(0, MAX_CONTENT_LENGTH2) + `
2146
+
2147
+ ... (content truncated, showing first ${MAX_CONTENT_LENGTH2} characters of ${stringified.length})`;
2148
+ }
2149
+ var requestIdCounter = Math.floor(Math.random() * 1e6);
2150
+ var sessionCache = /* @__PURE__ */ new Map();
2151
+ function generateRequestId() {
2152
+ return ++requestIdCounter;
2153
+ }
2154
+ function parseSSEResponse(text) {
2155
+ const events = text.split(/\n\n+/);
2156
+ let lastEventData = "";
2157
+ for (const event of events) {
2158
+ const dataLines = [];
2159
+ for (const line of event.split("\n")) {
2160
+ if (line.startsWith("data: ")) {
2161
+ dataLines.push(line.slice(6));
2162
+ } else if (line.startsWith("data:")) {
2163
+ dataLines.push(line.slice(5));
2404
2164
  }
2405
2165
  }
2406
- };
2166
+ if (dataLines.length > 0) {
2167
+ lastEventData = dataLines.join("\n");
2168
+ }
2169
+ }
2170
+ if (!lastEventData) {
2171
+ throw new Error("No data found in SSE response");
2172
+ }
2173
+ return JSON.parse(lastEventData);
2407
2174
  }
2408
- function createNotionGetTeamsTool(getAccessToken, requestNotionAccess) {
2409
- return {
2410
- type: "function",
2411
- function: {
2412
- name: "notion-get-teams",
2413
- description: "Retrieve teams (teamspaces) in the Notion workspace. Shows team names, IDs, membership status, and roles.",
2414
- arguments: {
2415
- type: "object",
2416
- properties: {
2417
- query: {
2418
- type: "string",
2419
- description: "Search query to filter teams by name (case-insensitive, max 100 chars)"
2420
- }
2421
- }
2422
- }
2423
- },
2424
- executor: async (args) => {
2425
- let token = getAccessToken();
2426
- if (!token) {
2427
- token = await requestNotionAccess();
2428
- }
2429
- try {
2430
- return await callMCPTool(token, "notion-get-teams", args);
2431
- } catch (error) {
2432
- return `Error retrieving Notion teams: ${error instanceof Error ? error.message : String(error)}`;
2175
+ async function parseResponseBody(response) {
2176
+ const contentType = response.headers.get("Content-Type") || "";
2177
+ const text = await response.text();
2178
+ if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
2179
+ return parseSSEResponse(text);
2180
+ }
2181
+ return JSON.parse(text);
2182
+ }
2183
+ async function initializeMCPSession(accessToken) {
2184
+ const requestId = generateRequestId();
2185
+ const initRequest = {
2186
+ jsonrpc: "2.0",
2187
+ id: requestId,
2188
+ method: "initialize",
2189
+ params: {
2190
+ protocolVersion: "2024-11-05",
2191
+ capabilities: {},
2192
+ clientInfo: {
2193
+ name: "Anuma",
2194
+ version: "1.0.0"
2433
2195
  }
2434
2196
  }
2435
2197
  };
2198
+ const response = await fetch(MCP_HTTP_ENDPOINT, {
2199
+ method: "POST",
2200
+ headers: {
2201
+ "Content-Type": "application/json",
2202
+ Accept: "application/json, text/event-stream",
2203
+ Authorization: `Bearer ${accessToken}`
2204
+ },
2205
+ body: JSON.stringify(initRequest)
2206
+ });
2207
+ if (!response.ok) {
2208
+ const errorBody = await response.text().catch(() => "");
2209
+ throw new Error(
2210
+ `MCP initialization failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
2211
+ );
2212
+ }
2213
+ const sessionId = response.headers.get("Mcp-Session-Id");
2214
+ if (!sessionId) {
2215
+ throw new Error("No Mcp-Session-Id returned from initialization");
2216
+ }
2217
+ const jsonRpcResponse = await parseResponseBody(response);
2218
+ if (jsonRpcResponse.error) {
2219
+ const err = jsonRpcResponse.error;
2220
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2221
+ throw new Error(`MCP initialization error: ${errMsg}`);
2222
+ }
2223
+ await fetch(MCP_HTTP_ENDPOINT, {
2224
+ method: "POST",
2225
+ headers: {
2226
+ "Content-Type": "application/json",
2227
+ Authorization: `Bearer ${accessToken}`,
2228
+ "Mcp-Session-Id": sessionId
2229
+ },
2230
+ body: JSON.stringify({
2231
+ jsonrpc: "2.0",
2232
+ method: "notifications/initialized"
2233
+ })
2234
+ });
2235
+ sessionCache.set(accessToken, sessionId);
2236
+ return sessionId;
2436
2237
  }
2437
- function createNotionTools(getAccessToken, requestNotionAccess) {
2438
- return [
2439
- // Search
2440
- createNotionSearchTool(getAccessToken, requestNotionAccess),
2441
- // Pages
2442
- createNotionFetchTool(getAccessToken, requestNotionAccess),
2443
- createNotionCreatePagesTool(getAccessToken, requestNotionAccess),
2444
- createNotionUpdatePageTool(getAccessToken, requestNotionAccess),
2445
- createNotionMovePagesTool(getAccessToken, requestNotionAccess),
2446
- createNotionDuplicatePageTool(getAccessToken, requestNotionAccess),
2447
- // Data Sources (Databases)
2448
- createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess),
2449
- createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess),
2450
- // Comments
2451
- createNotionCreateCommentTool(getAccessToken, requestNotionAccess),
2452
- createNotionGetCommentsTool(getAccessToken, requestNotionAccess),
2453
- // Users & Teams
2454
- createNotionGetUsersTool(getAccessToken, requestNotionAccess),
2455
- createNotionGetTeamsTool(getAccessToken, requestNotionAccess)
2456
- ];
2238
+ async function ensureMCPSession(accessToken) {
2239
+ const cached = sessionCache.get(accessToken);
2240
+ if (cached) {
2241
+ return cached;
2242
+ }
2243
+ return initializeMCPSession(accessToken);
2457
2244
  }
2458
- function getMCPEndpoints() {
2459
- return {
2460
- http: MCP_HTTP_ENDPOINT,
2461
- sse: MCP_SSE_ENDPOINT
2245
+ async function callMCPTool(accessToken, toolName, args) {
2246
+ const sessionId = await ensureMCPSession(accessToken);
2247
+ const requestId = generateRequestId();
2248
+ const jsonRpcRequest = {
2249
+ jsonrpc: "2.0",
2250
+ id: requestId,
2251
+ method: "tools/call",
2252
+ params: {
2253
+ name: toolName,
2254
+ arguments: args
2255
+ }
2462
2256
  };
2463
- }
2464
- async function callNotionMCPTool(accessToken, toolName, args) {
2465
- return callMCPTool(accessToken, toolName, args);
2466
- }
2467
-
2468
- // src/tools/github.ts
2469
- var GITHUB_API = "https://api.github.com";
2470
- var FETCH_TIMEOUT_MS = 3e4;
2471
- async function resolveToken(getAccessToken, requestGitHubAccess) {
2472
- let token = getAccessToken();
2473
- if (!token) {
2474
- try {
2475
- token = await requestGitHubAccess();
2476
- } catch (err) {
2477
- getLogger().error("Failed to resolve GitHub token", err);
2478
- return null;
2257
+ const response = await fetch(MCP_HTTP_ENDPOINT, {
2258
+ method: "POST",
2259
+ headers: {
2260
+ "Content-Type": "application/json",
2261
+ Accept: "application/json, text/event-stream",
2262
+ Authorization: `Bearer ${accessToken}`,
2263
+ "Mcp-Session-Id": sessionId
2264
+ },
2265
+ body: JSON.stringify(jsonRpcRequest)
2266
+ });
2267
+ if (!response.ok) {
2268
+ if (response.status === 400 || response.status === 401) {
2269
+ sessionCache.delete(accessToken);
2270
+ const newSessionId = await initializeMCPSession(accessToken);
2271
+ const retryResponse = await fetch(MCP_HTTP_ENDPOINT, {
2272
+ method: "POST",
2273
+ headers: {
2274
+ "Content-Type": "application/json",
2275
+ Accept: "application/json, text/event-stream",
2276
+ Authorization: `Bearer ${accessToken}`,
2277
+ "Mcp-Session-Id": newSessionId
2278
+ },
2279
+ body: JSON.stringify(jsonRpcRequest)
2280
+ });
2281
+ if (!retryResponse.ok) {
2282
+ const errorBody2 = await retryResponse.text().catch(() => "");
2283
+ throw new Error(
2284
+ `MCP request failed: ${retryResponse.status} ${retryResponse.statusText}${errorBody2 ? ` - ${errorBody2}` : ""}`
2285
+ );
2286
+ }
2287
+ const retryJsonRpcResponse = await parseResponseBody(retryResponse);
2288
+ if (retryJsonRpcResponse.error) {
2289
+ const err = retryJsonRpcResponse.error;
2290
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2291
+ throw new Error(`MCP tool error: ${errMsg}`);
2292
+ }
2293
+ return truncateToolResult(retryJsonRpcResponse.result);
2479
2294
  }
2295
+ const errorBody = await response.text().catch(() => "");
2296
+ throw new Error(
2297
+ `MCP request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
2298
+ );
2480
2299
  }
2481
- return token;
2482
- }
2483
- async function githubFetch(token, path, options = {}) {
2484
- if (!path.startsWith("http") && !path.startsWith("/")) {
2485
- throw new Error("githubFetch: relative path must start with /");
2300
+ const jsonRpcResponse = await parseResponseBody(response);
2301
+ if (jsonRpcResponse.error) {
2302
+ const err = jsonRpcResponse.error;
2303
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2304
+ throw new Error(`MCP tool error: ${errMsg}`);
2486
2305
  }
2487
- const url = path.startsWith("http") ? path : `${GITHUB_API}${path}`;
2488
- if (!url.startsWith(`${GITHUB_API}/`)) {
2489
- throw new Error("githubFetch: URL must target GitHub API");
2306
+ return truncateToolResult(jsonRpcResponse.result);
2307
+ }
2308
+ var NOTION_PROVIDER = "notion";
2309
+ function directRunner(getAccessToken, requestNotionAccess) {
2310
+ return async (toolName, args) => {
2311
+ let token = getAccessToken();
2312
+ if (!token) {
2313
+ token = await requestNotionAccess();
2314
+ }
2315
+ return callMCPTool(token, toolName, args);
2316
+ };
2317
+ }
2318
+ function readField(json, key) {
2319
+ return typeof json === "object" && json !== null ? json[key] : void 0;
2320
+ }
2321
+ function readString(json, key) {
2322
+ const value = readField(json, key);
2323
+ return typeof value === "string" ? value : void 0;
2324
+ }
2325
+ function notionConnectorError(status, json) {
2326
+ const code = readField(json, "code");
2327
+ if (status === 503) {
2328
+ return code === "upstream_unavailable" ? buildConnectorErrorResult("upstream_unavailable", NOTION_PROVIDER) : null;
2329
+ }
2330
+ if (status !== 401 && status !== 403 && status !== 412) return null;
2331
+ switch (code) {
2332
+ case "scope_not_covered": {
2333
+ const missingScopes = readField(json, "missing_scopes");
2334
+ return buildConnectorErrorResult(
2335
+ "scope_not_covered",
2336
+ NOTION_PROVIDER,
2337
+ readString(json, "connect_url"),
2338
+ Array.isArray(missingScopes) ? { missingScopes } : void 0
2339
+ );
2340
+ }
2341
+ case "insufficient_scope":
2342
+ return buildConnectorErrorResult("insufficient_scope", NOTION_PROVIDER, void 0, {
2343
+ required: readString(json, "required")
2344
+ });
2345
+ case "upstream_unavailable":
2346
+ return buildConnectorErrorResult("upstream_unavailable", NOTION_PROVIDER);
2347
+ default:
2348
+ return buildConnectorErrorResult(
2349
+ "connector_not_connected",
2350
+ NOTION_PROVIDER,
2351
+ readString(json, "connect_url")
2352
+ );
2490
2353
  }
2491
- const controller = new AbortController();
2492
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2493
- try {
2494
- return await fetch(url, {
2495
- ...options,
2496
- signal: controller.signal,
2497
- headers: {
2498
- Authorization: `Bearer ${token}`,
2499
- Accept: "application/vnd.github+json",
2500
- "Content-Type": "application/json",
2501
- "X-GitHub-Api-Version": "2022-11-28",
2502
- ...options.headers
2354
+ }
2355
+ function proxyRunner(callMcp) {
2356
+ return async (toolName, args) => {
2357
+ const { status, json } = await callMcp(toolName, args);
2358
+ if (status >= 200 && status < 300) {
2359
+ const result = readField(json, "result");
2360
+ if (result === void 0) {
2361
+ throw new Error(`Notion returned no result (${status})`);
2503
2362
  }
2504
- });
2505
- } finally {
2506
- clearTimeout(timeout);
2507
- }
2363
+ return truncateToolResult(result);
2364
+ }
2365
+ const connectorError = notionConnectorError(status, json);
2366
+ if (connectorError) return connectorError;
2367
+ const message = readField(json, "error");
2368
+ throw new Error(`${typeof message === "string" ? message : JSON.stringify(json)} (${status})`);
2369
+ };
2508
2370
  }
2509
- function createGetAuthenticatedUserTool(getAccessToken, requestAccess) {
2371
+ function withNotionExecutor(run, errorPrefix, tool) {
2510
2372
  return {
2511
- type: "function",
2512
- function: {
2513
- name: "github_get_authenticated_user",
2514
- description: "Get the authenticated GitHub user's profile including username and organizations. Call this first to discover the user's identity before making other GitHub API calls.",
2515
- parameters: {
2516
- type: "object",
2517
- properties: {}
2518
- }
2519
- },
2520
- executor: async () => {
2521
- const token = await resolveToken(getAccessToken, requestAccess);
2522
- if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2373
+ ...tool,
2374
+ executor: async (args) => {
2523
2375
  try {
2524
- const userResp = await githubFetch(token, "/user");
2525
- if (!userResp.ok) return `Error: Failed to get user (${userResp.status})`;
2526
- const user = await userResp.json();
2527
- const orgsResp = await githubFetch(token, "/user/orgs?per_page=100");
2528
- let orgs = [];
2529
- if (orgsResp.ok) {
2530
- const orgsData = await orgsResp.json();
2531
- orgs = orgsData.map((o) => o.login);
2532
- }
2533
- return {
2534
- login: user.login,
2535
- name: user.name,
2536
- email: user.email,
2537
- organizations: orgs,
2538
- public_repos: user.public_repos,
2539
- html_url: user.html_url
2540
- };
2541
- } catch (err) {
2542
- getLogger().error("github_get_authenticated_user error", err);
2543
- return "Error: Failed to get user profile.";
2376
+ return await run(tool.function.name, args);
2377
+ } catch (error) {
2378
+ return `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`;
2544
2379
  }
2545
2380
  }
2546
2381
  };
2547
2382
  }
2548
- var MAX_RESPONSE_SIZE = 1e5;
2549
- function truncate(text, maxLen) {
2550
- if (text.length <= maxLen) return text;
2551
- return text.slice(0, maxLen) + `
2552
-
2553
- ... (truncated, ${text.length - maxLen} characters omitted)`;
2554
- }
2555
- function createGitHubApiTool(getAccessToken, requestAccess) {
2556
- return {
2383
+ function notionSearchTool(run) {
2384
+ return withNotionExecutor(run, "Error searching Notion", {
2557
2385
  type: "function",
2558
2386
  function: {
2559
- name: "github_api",
2560
- description: `Make an authenticated request to the GitHub REST API (https://api.github.com). You can call any endpoint documented at https://docs.github.com/en/rest.
2561
-
2562
- Examples:
2563
- - List PRs: GET /repos/{owner}/{repo}/pulls?state=open
2564
- - Get file: GET /repos/{owner}/{repo}/contents/{path}
2565
- - Create issue: POST /repos/{owner}/{repo}/issues with body {"title": "...", "body": "..."}
2566
- - Search repos: GET /search/repositories?q={query}
2567
-
2568
- IMPORTANT: For write operations (POST, PUT, PATCH, DELETE), always confirm with the user before executing. This includes creating issues, opening PRs, merging, committing files, and submitting reviews.`,
2569
- parameters: {
2387
+ name: "notion-search",
2388
+ description: "Semantic search over Notion workspace and connected sources (Slack, Google Drive, Github, Jira, etc). Can also search for users by name or email.",
2389
+ arguments: {
2570
2390
  type: "object",
2571
2391
  properties: {
2572
- method: {
2392
+ query: {
2573
2393
  type: "string",
2574
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
2575
- description: "HTTP method"
2394
+ description: "Semantic search query over workspace and connected sources. For user search, a substring to match against name or email."
2576
2395
  },
2577
- path: {
2396
+ query_type: {
2578
2397
  type: "string",
2579
- description: "API path (e.g., /repos/owner/repo/pulls) or full URL for pagination"
2398
+ enum: ["internal", "user"],
2399
+ description: "Type of search: 'internal' for workspace/content search, 'user' for user search"
2580
2400
  },
2581
- body: {
2401
+ content_search_mode: {
2402
+ type: "string",
2403
+ enum: ["workspace_search", "ai_search"],
2404
+ description: "Override auto-selection of AI search vs workspace search"
2405
+ },
2406
+ data_source_url: {
2407
+ type: "string",
2408
+ description: "URL of a data source to search within"
2409
+ },
2410
+ page_url: {
2411
+ type: "string",
2412
+ description: "URL or ID of a page to search within"
2413
+ },
2414
+ teamspace_id: {
2415
+ type: "string",
2416
+ description: "ID of a teamspace to restrict search results to"
2417
+ },
2418
+ filters: {
2582
2419
  type: "object",
2583
- description: "Request body for POST/PUT/PATCH requests",
2584
- additionalProperties: true
2420
+ description: "Filters for search results (only for query_type 'internal'). Supports created_date_range {start_date, end_date} and created_by_user_ids array."
2585
2421
  }
2586
2422
  },
2587
- required: ["method", "path"]
2588
- }
2589
- },
2590
- executor: async (args) => {
2591
- const token = await resolveToken(getAccessToken, requestAccess);
2592
- if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2593
- const method = args.method;
2594
- const path = args.path;
2595
- const body = args.body;
2596
- try {
2597
- const resp = await githubFetch(token, path, {
2598
- method,
2599
- body: body ? JSON.stringify(body) : void 0
2600
- });
2601
- const text = await resp.text();
2602
- if (!resp.ok) {
2603
- return `Error: GitHub API ${method} ${path} returned ${resp.status}: ${truncate(text, 500)}`;
2604
- }
2605
- try {
2606
- const data = JSON.parse(text);
2607
- const json = JSON.stringify(data);
2608
- return truncate(json, MAX_RESPONSE_SIZE);
2609
- } catch {
2610
- return truncate(text, MAX_RESPONSE_SIZE);
2611
- }
2612
- } catch (err) {
2613
- if (err instanceof DOMException && err.name === "AbortError") {
2614
- return `Error: GitHub API request timed out after ${FETCH_TIMEOUT_MS / 1e3}s.`;
2615
- }
2616
- getLogger().error("github_api error", err);
2617
- return `Error: GitHub API request failed: ${err instanceof Error ? err.message : "unknown error"}`;
2423
+ required: ["query"]
2618
2424
  }
2619
2425
  }
2620
- };
2621
- }
2622
- function createGitHubTools(getAccessToken, requestGitHubAccess) {
2623
- return [
2624
- createGetAuthenticatedUserTool(getAccessToken, requestGitHubAccess),
2625
- createGitHubApiTool(getAccessToken, requestGitHubAccess)
2626
- ];
2627
- }
2628
-
2629
- // src/lib/connectors/agentres/errors.ts
2630
- var AgentresError = class extends Error {
2631
- constructor(status, code, message, retryable, nextStep) {
2632
- super(message);
2633
- this.name = "AgentresError";
2634
- this.status = status;
2635
- this.code = code;
2636
- this.retryable = retryable;
2637
- this.nextStep = nextStep;
2638
- }
2639
- };
2640
- var SiwxUnsupportedError = class extends Error {
2641
- constructor(message) {
2642
- super(message);
2643
- this.name = "SiwxUnsupportedError";
2644
- }
2645
- };
2646
- var AgentresPathError = class extends Error {
2647
- constructor(message) {
2648
- super(message);
2649
- this.name = "AgentresPathError";
2650
- }
2651
- };
2652
- var SiwxChallengeError = class extends Error {
2653
- constructor(message, options) {
2654
- super(message);
2655
- this.name = "SiwxChallengeError";
2656
- this.cause = options?.cause;
2657
- }
2658
- };
2659
- function parseAgentresError(status, body) {
2660
- const stated = statedError(body);
2661
- if (!stated) {
2662
- return new AgentresError(
2663
- status,
2664
- `HTTP_${status}`,
2665
- body.trim() || `HTTP ${status}`,
2666
- status >= 500
2667
- );
2668
- }
2669
- const code = typeof stated.code === "string" ? stated.code : `HTTP_${status}`;
2670
- const message = typeof stated.message === "string" ? stated.message : `HTTP ${status}`;
2671
- const retryable = typeof stated.retryable === "boolean" ? stated.retryable : status >= 500;
2672
- const nextStep = typeof stated.next_step === "string" ? stated.next_step : void 0;
2673
- return new AgentresError(status, code, message, retryable, nextStep);
2426
+ });
2674
2427
  }
2675
- function statedError(body) {
2676
- let parsed;
2677
- try {
2678
- parsed = JSON.parse(body);
2679
- } catch {
2680
- return null;
2681
- }
2682
- const envelope = parsed;
2683
- const stated = typeof envelope?.error === "object" && envelope.error !== null ? envelope.error : null;
2684
- if (!stated) {
2685
- return null;
2686
- }
2687
- const fields = stated;
2688
- if (typeof fields.code !== "string" && typeof fields.message !== "string") {
2689
- return null;
2690
- }
2691
- return fields;
2428
+ function createNotionSearchTool(getAccessToken, requestNotionAccess) {
2429
+ return notionSearchTool(directRunner(getAccessToken, requestNotionAccess));
2692
2430
  }
2693
-
2694
- // src/lib/processors/encoding.ts
2695
- var B64_ENCODE_CHUNK = 32768 * 3;
2696
- function uint8ArrayToBase64(data) {
2697
- if (typeof Buffer !== "undefined") {
2698
- return Buffer.from(data).toString("base64");
2699
- }
2700
- let base64 = "";
2701
- for (let i = 0; i < data.length; i += B64_ENCODE_CHUNK) {
2702
- const slice = data.subarray(i, i + B64_ENCODE_CHUNK);
2703
- let binary = "";
2704
- for (let j = 0; j < slice.length; j++) {
2705
- binary += String.fromCharCode(slice[j]);
2431
+ function notionFetchTool(run) {
2432
+ return withNotionExecutor(run, "Error fetching Notion page", {
2433
+ type: "function",
2434
+ function: {
2435
+ name: "notion-fetch",
2436
+ description: "Retrieve a Notion page or database by URL or ID. Pages return enhanced Markdown. Databases return all data sources.",
2437
+ arguments: {
2438
+ type: "object",
2439
+ properties: {
2440
+ id: {
2441
+ type: "string",
2442
+ description: "The ID or URL of the Notion page/database. Supports notion.so URLs, Notion Sites URLs (*.notion.site), and raw UUIDs."
2443
+ },
2444
+ include_transcript: {
2445
+ type: "boolean",
2446
+ description: "Include transcript if available"
2447
+ },
2448
+ include_discussions: {
2449
+ type: "boolean",
2450
+ description: "Include discussion counts and inline discussion markers"
2451
+ }
2452
+ },
2453
+ required: ["id"]
2454
+ }
2706
2455
  }
2707
- base64 += btoa(binary);
2708
- }
2709
- return base64;
2710
- }
2711
- function base64ToUint8Array(base64) {
2712
- if (typeof Buffer !== "undefined") {
2713
- const buf = Buffer.from(base64, "base64");
2714
- return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
2715
- }
2716
- const binary = atob(base64);
2717
- const bytes = new Uint8Array(binary.length);
2718
- for (let i = 0; i < binary.length; i++) {
2719
- bytes[i] = binary.charCodeAt(i);
2720
- }
2721
- return bytes;
2456
+ });
2722
2457
  }
2723
-
2724
- // src/lib/connectors/agentres/siwx.ts
2725
- var PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
2726
- var SIWX_HEADER = "SIGN-IN-WITH-X";
2727
- var SIWX_EXTENSION = "sign-in-with-x";
2728
- var SOLANA_NAMESPACE = "solana:";
2729
- var ED25519_SIGNATURE_BYTES = 64;
2730
- function parseChallenge(paymentRequiredHeader, expectedOrigin) {
2731
- const decoded = decodeHeader(paymentRequiredHeader);
2732
- const extensions = asRecord(decoded.extensions);
2733
- const extension = extensions ? asRecord(extensions[SIWX_EXTENSION]) : null;
2734
- if (!extension) {
2735
- throw new SiwxUnsupportedError(
2736
- "the 402 carries no sign-in-with-x extension, so this endpoint is paid rather than identity-only"
2737
- );
2738
- }
2739
- const info = asRecord(extension.info);
2740
- if (!info) {
2741
- throw new SiwxChallengeError("the sign-in-with-x extension has no info block");
2742
- }
2743
- const chain = solanaChain(extension.supportedChains);
2744
- const challenge = {
2745
- domain: requireString(info, "domain"),
2746
- uri: requireString(info, "uri"),
2747
- version: requireString(info, "version"),
2748
- statement: requireString(info, "statement"),
2749
- nonce: requireString(info, "nonce"),
2750
- issuedAt: requireString(info, "issuedAt"),
2751
- resources: requireResources(info),
2752
- chainId: chain.chainId,
2753
- signingType: chain.signingType
2754
- };
2755
- assertScopedTo(challenge, expectedOrigin);
2756
- return challenge;
2458
+ function createNotionFetchTool(getAccessToken, requestNotionAccess) {
2459
+ return notionFetchTool(directRunner(getAccessToken, requestNotionAccess));
2757
2460
  }
2758
- function assertScopedTo(challenge, expectedOrigin) {
2759
- const expected = parseUrl(expectedOrigin, `the agentres base URL "${expectedOrigin}"`);
2760
- if (challenge.domain !== expected.host) {
2761
- throw new SiwxChallengeError(
2762
- `the challenge is scoped to "${challenge.domain}" but this client talks to "${expected.host}" \u2014 refusing to sign it`
2763
- );
2764
- }
2765
- const uri = parseUrl(challenge.uri, `the sign-in-with-x uri "${challenge.uri}"`);
2766
- if (uri.origin !== expected.origin) {
2767
- throw new SiwxChallengeError(
2768
- `the challenge addresses "${uri.origin}" but this client talks to "${expected.origin}" \u2014 refusing to sign it`
2769
- );
2770
- }
2461
+ function notionCreatePagesTool(run) {
2462
+ return withNotionExecutor(run, "Error creating Notion page", {
2463
+ type: "function",
2464
+ function: {
2465
+ name: "notion-create-pages",
2466
+ description: "Create one or more Notion pages. All pages in a single call share the same parent. Properties use simplified format: title is a string, dates use date:{prop}:start format, checkboxes use __YES__/__NO__. Content is Notion-flavored Markdown.",
2467
+ arguments: {
2468
+ type: "object",
2469
+ properties: {
2470
+ pages: {
2471
+ type: "array",
2472
+ description: "Array of page objects to create (max 100)",
2473
+ items: {
2474
+ type: "object",
2475
+ properties: {
2476
+ properties: {
2477
+ type: "object",
2478
+ description: "JSON map of property names to values. Title is a string, numbers are JS numbers, checkboxes use __YES__/__NO__, dates use date:{prop}:start/end/is_datetime format."
2479
+ },
2480
+ content: {
2481
+ type: "string",
2482
+ description: "Page content in Notion-flavored Markdown format"
2483
+ }
2484
+ },
2485
+ required: ["properties", "content"]
2486
+ }
2487
+ },
2488
+ parent: {
2489
+ type: "object",
2490
+ description: "Parent for all pages. Use {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, or {type:'data_source_id', data_source_id:'...'}. Do NOT pass this field to create standalone private pages."
2491
+ }
2492
+ },
2493
+ required: ["pages"]
2494
+ }
2495
+ }
2496
+ });
2771
2497
  }
2772
- function parseUrl(value, description) {
2773
- try {
2774
- return new URL(value);
2775
- } catch (cause) {
2776
- throw new SiwxChallengeError(`${description} is not a URL`, { cause });
2777
- }
2498
+ function createNotionCreatePagesTool(getAccessToken, requestNotionAccess) {
2499
+ return notionCreatePagesTool(directRunner(getAccessToken, requestNotionAccess));
2778
2500
  }
2779
- function buildMessage(challenge, address) {
2780
- if (!challenge.chainId.startsWith(SOLANA_NAMESPACE)) {
2781
- throw new SiwxChallengeError(
2782
- `expected a solana: chain id for the message, got "${challenge.chainId}"`
2783
- );
2784
- }
2785
- const reference = challenge.chainId.slice(SOLANA_NAMESPACE.length);
2786
- const resources = challenge.resources.map((resource) => `- ${resource}`).join("\n");
2787
- return `${challenge.domain} wants you to sign in with your Solana account:
2788
- ${address}
2789
-
2790
- ${challenge.statement}
2791
-
2792
- URI: ${challenge.uri}
2793
- Version: ${challenge.version}
2794
- Chain ID: ${reference}
2795
- Nonce: ${challenge.nonce}
2796
- Issued At: ${challenge.issuedAt}
2797
- Resources:
2798
- ${resources}`;
2501
+ function notionUpdatePageTool(run) {
2502
+ return withNotionExecutor(run, "Error updating Notion page", {
2503
+ type: "function",
2504
+ function: {
2505
+ name: "notion-update-page",
2506
+ description: "Update a Notion page's properties or content. Uses command-based operations: 'update_properties' to change properties, 'replace_content' to replace all content, 'replace_content_range' to replace specific text, 'insert_content_after' to insert after text.",
2507
+ arguments: {
2508
+ type: "object",
2509
+ properties: {
2510
+ data: {
2511
+ type: "object",
2512
+ description: "Update data containing page_id and a command",
2513
+ properties: {
2514
+ page_id: {
2515
+ type: "string",
2516
+ description: "The ID of the page to update (with or without dashes)"
2517
+ },
2518
+ command: {
2519
+ type: "string",
2520
+ enum: [
2521
+ "update_properties",
2522
+ "replace_content",
2523
+ "replace_content_range",
2524
+ "insert_content_after"
2525
+ ],
2526
+ description: "The update command to execute"
2527
+ },
2528
+ properties: {
2529
+ type: "object",
2530
+ description: "For update_properties: JSON map of property names to values. Use null to remove a value."
2531
+ },
2532
+ new_str: {
2533
+ type: "string",
2534
+ description: "For replace_content/replace_content_range/insert_content_after: the new content string"
2535
+ },
2536
+ selection_with_ellipsis: {
2537
+ type: "string",
2538
+ description: "For replace_content_range/insert_content_after: unique start and end snippet (~10 chars each with ellipsis)"
2539
+ },
2540
+ allow_deleting_content: {
2541
+ type: "boolean",
2542
+ description: "For replace_content/replace_content_range: allow deletion of child pages/databases"
2543
+ }
2544
+ },
2545
+ required: ["page_id", "command"]
2546
+ }
2547
+ },
2548
+ required: ["data"]
2549
+ }
2550
+ }
2551
+ });
2799
2552
  }
2800
- function buildPayload(challenge, address, signature) {
2801
- if (signature.length !== ED25519_SIGNATURE_BYTES) {
2802
- throw new SiwxChallengeError(
2803
- `expected a ${ED25519_SIGNATURE_BYTES}-byte ed25519 signature, got ${signature.length} bytes`
2804
- );
2805
- }
2806
- const payload = {
2807
- domain: challenge.domain,
2808
- address,
2809
- statement: challenge.statement,
2810
- uri: challenge.uri,
2811
- version: challenge.version,
2812
- // Full CAIP-2 here, unlike the bare reference the message carries.
2813
- chainId: challenge.chainId,
2814
- type: challenge.signingType,
2815
- nonce: challenge.nonce,
2816
- issuedAt: challenge.issuedAt,
2817
- resources: challenge.resources,
2818
- signature: base58Encode(signature)
2819
- };
2820
- return uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload)));
2553
+ function createNotionUpdatePageTool(getAccessToken, requestNotionAccess) {
2554
+ return notionUpdatePageTool(directRunner(getAccessToken, requestNotionAccess));
2821
2555
  }
2822
- var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2823
- function base58Encode(bytes) {
2824
- let zeros = 0;
2825
- while (zeros < bytes.length && bytes[zeros] === 0) {
2826
- zeros++;
2827
- }
2828
- const digits = [];
2829
- for (let i = zeros; i < bytes.length; i++) {
2830
- let carry = bytes[i];
2831
- for (let j = 0; j < digits.length; j++) {
2832
- carry += digits[j] * 256;
2833
- digits[j] = carry % 58;
2834
- carry = Math.floor(carry / 58);
2556
+ function notionMovePagesTool(run) {
2557
+ return withNotionExecutor(run, "Error moving Notion pages", {
2558
+ type: "function",
2559
+ function: {
2560
+ name: "notion-move-pages",
2561
+ description: "Move one or more Notion pages or databases to a new parent.",
2562
+ arguments: {
2563
+ type: "object",
2564
+ properties: {
2565
+ page_or_database_ids: {
2566
+ type: "array",
2567
+ description: "Array of page or database IDs to move (UUIDs, max 100)",
2568
+ items: { type: "string" }
2569
+ },
2570
+ new_parent: {
2571
+ type: "object",
2572
+ description: "New parent: {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, {type:'data_source_id', data_source_id:'...'}, or {type:'workspace'} for private pages."
2573
+ }
2574
+ },
2575
+ required: ["page_or_database_ids", "new_parent"]
2576
+ }
2835
2577
  }
2836
- while (carry > 0) {
2837
- digits.push(carry % 58);
2838
- carry = Math.floor(carry / 58);
2578
+ });
2579
+ }
2580
+ function createNotionMovePagesTool(getAccessToken, requestNotionAccess) {
2581
+ return notionMovePagesTool(directRunner(getAccessToken, requestNotionAccess));
2582
+ }
2583
+ function notionDuplicatePageTool(run) {
2584
+ return withNotionExecutor(run, "Error duplicating Notion page", {
2585
+ type: "function",
2586
+ function: {
2587
+ name: "notion-duplicate-page",
2588
+ description: "Duplicate a Notion page. The duplication completes asynchronously.",
2589
+ arguments: {
2590
+ type: "object",
2591
+ properties: {
2592
+ page_id: {
2593
+ type: "string",
2594
+ description: "The ID of the page to duplicate (UUID with or without dashes)"
2595
+ }
2596
+ },
2597
+ required: ["page_id"]
2598
+ }
2839
2599
  }
2840
- }
2841
- let encoded = "1".repeat(zeros);
2842
- for (let i = digits.length - 1; i >= 0; i--) {
2843
- encoded += BASE58_ALPHABET[digits[i]];
2844
- }
2845
- return encoded;
2600
+ });
2846
2601
  }
2847
- function decodeHeader(paymentRequiredHeader) {
2848
- let parsed;
2849
- try {
2850
- parsed = JSON.parse(new TextDecoder().decode(base64ToUint8Array(paymentRequiredHeader)));
2851
- } catch (cause) {
2852
- throw new SiwxChallengeError("the PAYMENT-REQUIRED header is not base64-encoded JSON", {
2853
- cause
2854
- });
2855
- }
2856
- const decoded = asRecord(parsed);
2857
- if (!decoded) {
2858
- throw new SiwxChallengeError("the PAYMENT-REQUIRED header did not decode to an object");
2859
- }
2860
- return decoded;
2602
+ function createNotionDuplicatePageTool(getAccessToken, requestNotionAccess) {
2603
+ return notionDuplicatePageTool(directRunner(getAccessToken, requestNotionAccess));
2861
2604
  }
2862
- function solanaChain(supportedChains) {
2863
- if (!Array.isArray(supportedChains)) {
2864
- throw new SiwxChallengeError("the sign-in-with-x extension lists no supportedChains");
2865
- }
2866
- for (const entry of supportedChains) {
2867
- const chain = asRecord(entry);
2868
- const chainId = chain?.chainId;
2869
- if (typeof chainId !== "string" || !chainId.startsWith(SOLANA_NAMESPACE)) {
2870
- continue;
2871
- }
2872
- const signingType = chain?.type;
2873
- if (typeof signingType !== "string" || signingType === "") {
2874
- throw new SiwxChallengeError(`the ${chainId} entry declares no signing type`);
2605
+ function notionCreateDatabaseTool(run) {
2606
+ return withNotionExecutor(run, "Error creating Notion database", {
2607
+ type: "function",
2608
+ function: {
2609
+ name: "notion-create-database",
2610
+ description: "Create a new Notion database with a properties schema. If no title property is provided, 'Name' is auto-added. Supports property types: title, rich_text, number, select, multi_select, date, people, checkbox, url, email, phone_number, formula, relation, rollup, status, unique_id, etc.",
2611
+ arguments: {
2612
+ type: "object",
2613
+ properties: {
2614
+ properties: {
2615
+ type: "object",
2616
+ description: "Property schema for the database. Each key is a property name, value defines the type."
2617
+ },
2618
+ parent: {
2619
+ type: "object",
2620
+ description: "Parent page: {type:'page_id', page_id:'...'}. Omit for private workspace-level database."
2621
+ },
2622
+ title: {
2623
+ type: "array",
2624
+ description: "Title of the database as rich text array (max 100)",
2625
+ items: { type: "object" }
2626
+ },
2627
+ description: {
2628
+ type: "array",
2629
+ description: "Description of the database as rich text array (max 100)",
2630
+ items: { type: "object" }
2631
+ }
2632
+ },
2633
+ required: ["properties"]
2634
+ }
2875
2635
  }
2876
- return { chainId, signingType };
2877
- }
2878
- throw new SiwxChallengeError("the sign-in-with-x extension offers no solana: chain");
2636
+ });
2879
2637
  }
2880
- function asRecord(value) {
2881
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2638
+ function createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess) {
2639
+ return notionCreateDatabaseTool(directRunner(getAccessToken, requestNotionAccess));
2882
2640
  }
2883
- function requireString(info, key) {
2884
- const value = info[key];
2885
- if (typeof value !== "string" || value === "") {
2886
- throw new SiwxChallengeError(`the sign-in-with-x info block has no ${key}`);
2887
- }
2888
- return value;
2641
+ function notionUpdateDataSourceTool(run) {
2642
+ return withNotionExecutor(run, "Error updating Notion data source", {
2643
+ type: "function",
2644
+ function: {
2645
+ name: "notion-update-data-source",
2646
+ description: "Update a Notion data source's title, description, property schema, or other attributes. Use null to remove a property. Provide only 'name' to rename a property.",
2647
+ arguments: {
2648
+ type: "object",
2649
+ properties: {
2650
+ data_source_id: {
2651
+ type: "string",
2652
+ description: "The ID of the data source to update (UUID). Can be a data source ID or database ID."
2653
+ },
2654
+ title: {
2655
+ type: "array",
2656
+ description: "New title as rich text array (max 100)",
2657
+ items: { type: "object" }
2658
+ },
2659
+ description: {
2660
+ type: "array",
2661
+ description: "New description as rich text array (max 100)",
2662
+ items: { type: "object" }
2663
+ },
2664
+ properties: {
2665
+ type: "object",
2666
+ description: "Property schema updates. Use null to remove, {name:'...'} to rename, or full definition to add/update."
2667
+ },
2668
+ is_inline: {
2669
+ type: "boolean",
2670
+ description: "Whether the database is inline"
2671
+ },
2672
+ in_trash: {
2673
+ type: "boolean",
2674
+ description: "Trash the data source (requires Notion UI to undo)"
2675
+ }
2676
+ },
2677
+ required: ["data_source_id"]
2678
+ }
2679
+ }
2680
+ });
2889
2681
  }
2890
- function requireResources(info) {
2891
- const resources = info.resources;
2892
- if (!Array.isArray(resources) || resources.length === 0) {
2893
- throw new SiwxChallengeError("the sign-in-with-x info block has no resources");
2894
- }
2895
- if (!resources.every((resource) => typeof resource === "string" && resource !== "")) {
2896
- throw new SiwxChallengeError("the sign-in-with-x resources are not all strings");
2897
- }
2898
- return resources;
2682
+ function createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess) {
2683
+ return notionUpdateDataSourceTool(directRunner(getAccessToken, requestNotionAccess));
2899
2684
  }
2900
-
2901
- // src/lib/connectors/agentres/client.ts
2902
- var DEFAULT_BASE_URL = "https://agentres.dev";
2903
- function createAgentresClient(options) {
2904
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
2905
- const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
2906
- const { address, signMessage } = options;
2907
- async function withSiwx(request) {
2908
- const url = resolveUrl(baseUrl, request.path);
2909
- const init = requestInit(request);
2910
- const challenged = await fetchImpl(url, init);
2911
- if (challenged.status !== 402) {
2912
- return readBody(challenged);
2685
+ function notionCreateCommentTool(run) {
2686
+ return withNotionExecutor(run, "Error creating Notion comment", {
2687
+ type: "function",
2688
+ function: {
2689
+ name: "notion-create-comment",
2690
+ description: "Add a comment to a Notion page. Supports page-level comments, content-specific comments (using selection_with_ellipsis), or replies to existing discussions (using discussion_id).",
2691
+ arguments: {
2692
+ type: "object",
2693
+ properties: {
2694
+ page_id: {
2695
+ type: "string",
2696
+ description: "The ID of the page to comment on (with or without dashes)"
2697
+ },
2698
+ rich_text: {
2699
+ type: "array",
2700
+ description: "Comment content as rich text array (max 100). Objects can be text, mention, or equation.",
2701
+ items: {
2702
+ type: "object",
2703
+ properties: {
2704
+ text: {
2705
+ type: "object",
2706
+ properties: {
2707
+ content: { type: "string" }
2708
+ },
2709
+ required: ["content"]
2710
+ }
2711
+ },
2712
+ required: ["text"]
2713
+ }
2714
+ },
2715
+ discussion_id: {
2716
+ type: "string",
2717
+ description: "ID or URL of an existing discussion to reply to"
2718
+ },
2719
+ selection_with_ellipsis: {
2720
+ type: "string",
2721
+ description: "Unique start and end snippet (~10 chars each with ellipsis) of content to comment on"
2722
+ }
2723
+ },
2724
+ required: ["page_id", "rich_text"]
2725
+ }
2913
2726
  }
2914
- await challenged.body?.cancel().catch(() => {
2915
- });
2916
- const header = challenged.headers.get(PAYMENT_REQUIRED_HEADER);
2917
- if (!header) {
2918
- throw new SiwxChallengeError(
2919
- `the 402 from ${request.path} carried no ${PAYMENT_REQUIRED_HEADER} header`
2920
- );
2727
+ });
2728
+ }
2729
+ function createNotionCreateCommentTool(getAccessToken, requestNotionAccess) {
2730
+ return notionCreateCommentTool(directRunner(getAccessToken, requestNotionAccess));
2731
+ }
2732
+ function notionGetCommentsTool(run) {
2733
+ return withNotionExecutor(run, "Error retrieving Notion comments", {
2734
+ type: "function",
2735
+ function: {
2736
+ name: "notion-get-comments",
2737
+ description: "Get comments and discussions from a Notion page. Returns discussions with full comment content.",
2738
+ arguments: {
2739
+ type: "object",
2740
+ properties: {
2741
+ page_id: {
2742
+ type: "string",
2743
+ description: "Identifier for the Notion page"
2744
+ },
2745
+ include_all_blocks: {
2746
+ type: "boolean",
2747
+ description: "Include discussions on child blocks (default: false)"
2748
+ },
2749
+ include_resolved: {
2750
+ type: "boolean",
2751
+ description: "Include resolved discussions (default: false)"
2752
+ },
2753
+ discussion_id: {
2754
+ type: "string",
2755
+ description: "Fetch a specific discussion by ID or URL (e.g., discussion://pageId/blockId/discussionId)"
2756
+ }
2757
+ },
2758
+ required: ["page_id"]
2759
+ }
2921
2760
  }
2922
- const challenge = parseChallenge(header, baseUrl);
2923
- const signature = await signMessage(new TextEncoder().encode(buildMessage(challenge, address)));
2924
- const authorized = await fetchImpl(url, {
2925
- ...init,
2926
- headers: { ...init.headers, [SIWX_HEADER]: buildPayload(challenge, address, signature) }
2927
- });
2928
- return readBody(authorized);
2929
- }
2930
- async function linkResy(body) {
2931
- const response = await withSiwx({ method: "POST", path: "/api/link-resy", body });
2932
- return { step: response.step, message: response.message, resyUserId: response.resy_user_id };
2933
- }
2934
- return {
2935
- withSiwx,
2936
- async ensureAccount(email) {
2937
- const body = await withSiwx({ method: "POST", path: "/api/account", body: { email } });
2938
- return {
2939
- accountId: body.account_id,
2940
- email: body.email,
2941
- credentialKind: body.credential_kind,
2942
- walletLinked: body.wallet_linked
2943
- };
2944
- },
2945
- requestCode(email) {
2946
- return linkResy({ em_address: email });
2947
- },
2948
- verifyCode(email, code) {
2949
- return linkResy({ em_address: email, code });
2950
- },
2951
- async getLinkStatus() {
2952
- const body = await withSiwx({ method: "GET", path: "/api/me" });
2953
- return {
2954
- accountId: body.account_id,
2955
- email: body.email,
2956
- resyLinked: body.resy_linked,
2957
- walletAddress: body.wallet_address,
2958
- resyLinkedAt: body.resy_linked_at ?? null
2959
- };
2761
+ });
2762
+ }
2763
+ function createNotionGetCommentsTool(getAccessToken, requestNotionAccess) {
2764
+ return notionGetCommentsTool(directRunner(getAccessToken, requestNotionAccess));
2765
+ }
2766
+ function notionGetUsersTool(run) {
2767
+ return withNotionExecutor(run, "Error listing Notion users", {
2768
+ type: "function",
2769
+ function: {
2770
+ name: "notion-get-users",
2771
+ description: "List users in the Notion workspace. Supports filtering by name/email, pagination, fetching a specific user by ID, or fetching the current bot user (user_id: 'self').",
2772
+ arguments: {
2773
+ type: "object",
2774
+ properties: {
2775
+ query: {
2776
+ type: "string",
2777
+ description: "Search query to filter users by name or email (case-insensitive, max 100 chars)"
2778
+ },
2779
+ start_cursor: {
2780
+ type: "string",
2781
+ description: "Cursor for pagination from previous response"
2782
+ },
2783
+ page_size: {
2784
+ type: "integer",
2785
+ description: "Number of users per page (1-100, default 100)"
2786
+ },
2787
+ user_id: {
2788
+ type: "string",
2789
+ description: "Return only the user matching this ID. Pass 'self' to fetch the current bot user."
2790
+ }
2791
+ }
2792
+ }
2793
+ }
2794
+ });
2795
+ }
2796
+ function createNotionGetUsersTool(getAccessToken, requestNotionAccess) {
2797
+ return notionGetUsersTool(directRunner(getAccessToken, requestNotionAccess));
2798
+ }
2799
+ function notionGetTeamsTool(run) {
2800
+ return withNotionExecutor(run, "Error retrieving Notion teams", {
2801
+ type: "function",
2802
+ function: {
2803
+ name: "notion-get-teams",
2804
+ description: "Retrieve teams (teamspaces) in the Notion workspace. Shows team names, IDs, membership status, and roles.",
2805
+ arguments: {
2806
+ type: "object",
2807
+ properties: {
2808
+ query: {
2809
+ type: "string",
2810
+ description: "Search query to filter teams by name (case-insensitive, max 100 chars)"
2811
+ }
2812
+ }
2813
+ }
2960
2814
  }
2815
+ });
2816
+ }
2817
+ function createNotionGetTeamsTool(getAccessToken, requestNotionAccess) {
2818
+ return notionGetTeamsTool(directRunner(getAccessToken, requestNotionAccess));
2819
+ }
2820
+ var NOTION_TOOLS = [
2821
+ // Search
2822
+ notionSearchTool,
2823
+ // Pages
2824
+ notionFetchTool,
2825
+ notionCreatePagesTool,
2826
+ notionUpdatePageTool,
2827
+ notionMovePagesTool,
2828
+ notionDuplicatePageTool,
2829
+ // Data Sources (Databases)
2830
+ notionCreateDatabaseTool,
2831
+ notionUpdateDataSourceTool,
2832
+ // Comments
2833
+ notionCreateCommentTool,
2834
+ notionGetCommentsTool,
2835
+ // Users & Teams
2836
+ notionGetUsersTool,
2837
+ notionGetTeamsTool
2838
+ ];
2839
+ function createNotionTools(getAccessToken, requestNotionAccess) {
2840
+ const run = directRunner(getAccessToken, requestNotionAccess);
2841
+ return NOTION_TOOLS.map((tool) => tool(run));
2842
+ }
2843
+ function createNotionProxyTools(callMcp) {
2844
+ const run = proxyRunner(callMcp);
2845
+ return NOTION_TOOLS.map((tool) => tool(run));
2846
+ }
2847
+ function getMCPEndpoints() {
2848
+ return {
2849
+ http: MCP_HTTP_ENDPOINT,
2850
+ sse: MCP_SSE_ENDPOINT
2961
2851
  };
2962
2852
  }
2963
- function resolveUrl(baseUrl, path) {
2964
- if (!path.startsWith("/") || path.startsWith("//")) {
2965
- throw new AgentresPathError(
2966
- `"${path}" is not a path on ${baseUrl} \u2014 it must start with a single "/"`
2967
- );
2968
- }
2969
- const base = new URL(baseUrl);
2970
- const resolved = new URL(path, base);
2971
- if (resolved.origin !== base.origin) {
2972
- throw new AgentresPathError(
2973
- `"${path}" resolves to ${resolved.origin} but this client talks to ${base.origin} \u2014 refusing to send it`
2974
- );
2975
- }
2976
- return resolved.toString();
2853
+ async function callNotionMCPTool(accessToken, toolName, args) {
2854
+ return callMCPTool(accessToken, toolName, args);
2977
2855
  }
2978
- function requestInit(request) {
2979
- const headers = { Accept: "application/json" };
2980
- if (request.body === void 0) {
2981
- return { method: request.method, headers };
2856
+
2857
+ // src/tools/github.ts
2858
+ var GITHUB_API = "https://api.github.com";
2859
+ var FETCH_TIMEOUT_MS = 3e4;
2860
+ async function resolveToken(getAccessToken, requestGitHubAccess) {
2861
+ let token = getAccessToken();
2862
+ if (!token) {
2863
+ try {
2864
+ token = await requestGitHubAccess();
2865
+ } catch (err) {
2866
+ getLogger().error("Failed to resolve GitHub token", err);
2867
+ return null;
2868
+ }
2982
2869
  }
2983
- headers["Content-Type"] = "application/json";
2984
- return { method: request.method, headers, body: JSON.stringify(request.body) };
2870
+ return token;
2985
2871
  }
2986
- async function readBody(response) {
2987
- const body = await response.text();
2988
- if (!response.ok) {
2989
- throw parseAgentresError(response.status, body);
2872
+ async function githubFetch(token, path, options = {}) {
2873
+ if (!path.startsWith("http") && !path.startsWith("/")) {
2874
+ throw new Error("githubFetch: relative path must start with /");
2875
+ }
2876
+ const url = path.startsWith("http") ? path : `${GITHUB_API}${path}`;
2877
+ if (!url.startsWith(`${GITHUB_API}/`)) {
2878
+ throw new Error("githubFetch: URL must target GitHub API");
2990
2879
  }
2880
+ const controller = new AbortController();
2881
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2991
2882
  try {
2992
- return JSON.parse(body);
2993
- } catch {
2994
- throw new AgentresError(
2995
- response.status,
2996
- "INVALID_JSON",
2997
- `agentres answered ${response.status} with a body that is not JSON`,
2998
- response.status >= 500
2999
- );
2883
+ return await fetch(url, {
2884
+ ...options,
2885
+ signal: controller.signal,
2886
+ headers: {
2887
+ Authorization: `Bearer ${token}`,
2888
+ Accept: "application/vnd.github+json",
2889
+ "Content-Type": "application/json",
2890
+ "X-GitHub-Api-Version": "2022-11-28",
2891
+ ...options.headers
2892
+ }
2893
+ });
2894
+ } finally {
2895
+ clearTimeout(timeout);
3000
2896
  }
3001
2897
  }
3002
-
3003
- // src/lib/connectors/client.ts
3004
- var DEFAULT_REFRESH_BEFORE_MS = 3e4;
3005
- function createConnectorTokenGetter(client, provider, opts = {}) {
3006
- const refreshBeforeMs = opts.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
3007
- const now = opts.now ?? Date.now;
3008
- let cached = null;
3009
- return async () => {
3010
- if (cached && cached.expiresAt - refreshBeforeMs > now()) {
3011
- return cached.accessToken;
3012
- }
3013
- let result;
3014
- try {
3015
- result = await client.mintConnectorToken(provider);
3016
- } catch {
3017
- cached = null;
3018
- return null;
3019
- }
3020
- if (result.ok) {
3021
- cached = { accessToken: result.accessToken, expiresAt: result.expiresAt };
3022
- return result.accessToken;
2898
+ function createGetAuthenticatedUserTool(getAccessToken, requestAccess) {
2899
+ return {
2900
+ type: "function",
2901
+ function: {
2902
+ name: "github_get_authenticated_user",
2903
+ description: "Get the authenticated GitHub user's profile including username and organizations. Call this first to discover the user's identity before making other GitHub API calls.",
2904
+ parameters: {
2905
+ type: "object",
2906
+ properties: {}
2907
+ }
2908
+ },
2909
+ executor: async () => {
2910
+ const token = await resolveToken(getAccessToken, requestAccess);
2911
+ if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2912
+ try {
2913
+ const userResp = await githubFetch(token, "/user");
2914
+ if (!userResp.ok) return `Error: Failed to get user (${userResp.status})`;
2915
+ const user = await userResp.json();
2916
+ const orgsResp = await githubFetch(token, "/user/orgs?per_page=100");
2917
+ let orgs = [];
2918
+ if (orgsResp.ok) {
2919
+ const orgsData = await orgsResp.json();
2920
+ orgs = orgsData.map((o) => o.login);
2921
+ }
2922
+ return {
2923
+ login: user.login,
2924
+ name: user.name,
2925
+ email: user.email,
2926
+ organizations: orgs,
2927
+ public_repos: user.public_repos,
2928
+ html_url: user.html_url
2929
+ };
2930
+ } catch (err) {
2931
+ getLogger().error("github_get_authenticated_user error", err);
2932
+ return "Error: Failed to get user profile.";
2933
+ }
3023
2934
  }
3024
- cached = null;
3025
- if (opts.onNotConnected) {
3026
- return opts.onNotConnected(result.error);
2935
+ };
2936
+ }
2937
+ var MAX_RESPONSE_SIZE = 1e5;
2938
+ function truncate(text, maxLen) {
2939
+ if (text.length <= maxLen) return text;
2940
+ return text.slice(0, maxLen) + `
2941
+
2942
+ ... (truncated, ${text.length - maxLen} characters omitted)`;
2943
+ }
2944
+ function createGitHubApiTool(getAccessToken, requestAccess) {
2945
+ return {
2946
+ type: "function",
2947
+ function: {
2948
+ name: "github_api",
2949
+ description: `Make an authenticated request to the GitHub REST API (https://api.github.com). You can call any endpoint documented at https://docs.github.com/en/rest.
2950
+
2951
+ Examples:
2952
+ - List PRs: GET /repos/{owner}/{repo}/pulls?state=open
2953
+ - Get file: GET /repos/{owner}/{repo}/contents/{path}
2954
+ - Create issue: POST /repos/{owner}/{repo}/issues with body {"title": "...", "body": "..."}
2955
+ - Search repos: GET /search/repositories?q={query}
2956
+
2957
+ IMPORTANT: For write operations (POST, PUT, PATCH, DELETE), always confirm with the user before executing. This includes creating issues, opening PRs, merging, committing files, and submitting reviews.`,
2958
+ parameters: {
2959
+ type: "object",
2960
+ properties: {
2961
+ method: {
2962
+ type: "string",
2963
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
2964
+ description: "HTTP method"
2965
+ },
2966
+ path: {
2967
+ type: "string",
2968
+ description: "API path (e.g., /repos/owner/repo/pulls) or full URL for pagination"
2969
+ },
2970
+ body: {
2971
+ type: "object",
2972
+ description: "Request body for POST/PUT/PATCH requests",
2973
+ additionalProperties: true
2974
+ }
2975
+ },
2976
+ required: ["method", "path"]
2977
+ }
2978
+ },
2979
+ executor: async (args) => {
2980
+ const token = await resolveToken(getAccessToken, requestAccess);
2981
+ if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2982
+ const method = args.method;
2983
+ const path = args.path;
2984
+ const body = args.body;
2985
+ try {
2986
+ const resp = await githubFetch(token, path, {
2987
+ method,
2988
+ body: body ? JSON.stringify(body) : void 0
2989
+ });
2990
+ const text = await resp.text();
2991
+ if (!resp.ok) {
2992
+ return `Error: GitHub API ${method} ${path} returned ${resp.status}: ${truncate(text, 500)}`;
2993
+ }
2994
+ try {
2995
+ const data = JSON.parse(text);
2996
+ const json = JSON.stringify(data);
2997
+ return truncate(json, MAX_RESPONSE_SIZE);
2998
+ } catch {
2999
+ return truncate(text, MAX_RESPONSE_SIZE);
3000
+ }
3001
+ } catch (err) {
3002
+ if (err instanceof DOMException && err.name === "AbortError") {
3003
+ return `Error: GitHub API request timed out after ${FETCH_TIMEOUT_MS / 1e3}s.`;
3004
+ }
3005
+ getLogger().error("github_api error", err);
3006
+ return `Error: GitHub API request failed: ${err instanceof Error ? err.message : "unknown error"}`;
3007
+ }
3027
3008
  }
3028
- return null;
3029
3009
  };
3030
3010
  }
3011
+ function createGitHubTools(getAccessToken, requestGitHubAccess) {
3012
+ return [
3013
+ createGetAuthenticatedUserTool(getAccessToken, requestGitHubAccess),
3014
+ createGitHubApiTool(getAccessToken, requestGitHubAccess)
3015
+ ];
3016
+ }
3031
3017
 
3032
3018
  // src/tools/gmail.ts
3033
3019
  var GMAIL_PROVIDER = "gmail";
@@ -3890,7 +3876,7 @@ async function callSlack(callProxy, path, query, requestBody) {
3890
3876
  const { status, json } = await callProxy(path, query, requestBody);
3891
3877
  return interpretSlackResult(path, status, json);
3892
3878
  }
3893
- function readString(raw) {
3879
+ function readString2(raw) {
3894
3880
  return typeof raw === "string" ? raw : "";
3895
3881
  }
3896
3882
  function clampLimit(raw, fallback, min, max) {
@@ -4452,7 +4438,7 @@ function createSlackSearchMessagesTool(callProxy) {
4452
4438
  }
4453
4439
  },
4454
4440
  executor: async (args) => searchSlackMessages(callProxy, {
4455
- query: readString(args.query),
4441
+ query: readString2(args.query),
4456
4442
  count: args.count,
4457
4443
  channel: typeof args.channel === "string" ? args.channel : void 0,
4458
4444
  from_user: typeof args.from_user === "string" ? args.from_user : void 0,
@@ -4529,7 +4515,7 @@ function createSlackGetChannelHistoryTool(callProxy) {
4529
4515
  }
4530
4516
  },
4531
4517
  executor: async (args) => getSlackChannelHistory(callProxy, {
4532
- channel: readString(args.channel),
4518
+ channel: readString2(args.channel),
4533
4519
  limit: args.limit
4534
4520
  })
4535
4521
  };
@@ -4560,8 +4546,8 @@ function createSlackGetThreadRepliesTool(callProxy) {
4560
4546
  }
4561
4547
  },
4562
4548
  executor: async (args) => getSlackThreadReplies(callProxy, {
4563
- channel: readString(args.channel),
4564
- ts: readString(args.ts),
4549
+ channel: readString2(args.channel),
4550
+ ts: readString2(args.ts),
4565
4551
  limit: args.limit
4566
4552
  })
4567
4553
  };
@@ -4592,8 +4578,8 @@ function createSlackPostMessageTool(callProxy) {
4592
4578
  }
4593
4579
  },
4594
4580
  executor: async (args) => postSlackMessage(callProxy, {
4595
- channel: readString(args.channel),
4596
- text: readString(args.text),
4581
+ channel: readString2(args.channel),
4582
+ text: readString2(args.text),
4597
4583
  thread_ts: typeof args.thread_ts === "string" ? args.thread_ts : void 0
4598
4584
  })
4599
4585
  };
@@ -14206,6 +14192,7 @@ function createDocumentTools({
14206
14192
  createNotionGetTeamsTool,
14207
14193
  createNotionGetUsersTool,
14208
14194
  createNotionMovePagesTool,
14195
+ createNotionProxyTools,
14209
14196
  createNotionSearchTool,
14210
14197
  createNotionTools,
14211
14198
  createNotionUpdateDataSourceTool,