@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.
@@ -1573,1306 +1573,1291 @@ function createDriveTools(getAccessToken, requestDriveAccess) {
1573
1573
  ];
1574
1574
  }
1575
1575
 
1576
- // src/tools/notion.ts
1577
- var MCP_HTTP_ENDPOINT = "https://mcp.notion.com/mcp";
1578
- var MCP_SSE_ENDPOINT = "https://mcp.notion.com/sse";
1579
- var MAX_CONTENT_LENGTH2 = 5e4;
1580
- function truncateToolResult(result) {
1581
- if (result === void 0 || result === null) return result;
1582
- const stringified = typeof result === "string" ? result : JSON.stringify(result);
1583
- if (stringified.length <= MAX_CONTENT_LENGTH2) {
1584
- return result;
1576
+ // src/lib/connectors/agentres/errors.ts
1577
+ var AgentresError = class extends Error {
1578
+ constructor(status, code, message, retryable, nextStep) {
1579
+ super(message);
1580
+ this.name = "AgentresError";
1581
+ this.status = status;
1582
+ this.code = code;
1583
+ this.retryable = retryable;
1584
+ this.nextStep = nextStep;
1585
1585
  }
1586
- return stringified.slice(0, MAX_CONTENT_LENGTH2) + `
1587
-
1588
- ... (content truncated, showing first ${MAX_CONTENT_LENGTH2} characters of ${stringified.length})`;
1586
+ };
1587
+ var SiwxUnsupportedError = class extends Error {
1588
+ constructor(message) {
1589
+ super(message);
1590
+ this.name = "SiwxUnsupportedError";
1591
+ }
1592
+ };
1593
+ var AgentresPathError = class extends Error {
1594
+ constructor(message) {
1595
+ super(message);
1596
+ this.name = "AgentresPathError";
1597
+ }
1598
+ };
1599
+ var SiwxChallengeError = class extends Error {
1600
+ constructor(message, options) {
1601
+ super(message);
1602
+ this.name = "SiwxChallengeError";
1603
+ this.cause = options?.cause;
1604
+ }
1605
+ };
1606
+ function parseAgentresError(status, body) {
1607
+ const stated = statedError(body);
1608
+ if (!stated) {
1609
+ return new AgentresError(
1610
+ status,
1611
+ `HTTP_${status}`,
1612
+ body.trim() || `HTTP ${status}`,
1613
+ status >= 500
1614
+ );
1615
+ }
1616
+ const code = typeof stated.code === "string" ? stated.code : `HTTP_${status}`;
1617
+ const message = typeof stated.message === "string" ? stated.message : `HTTP ${status}`;
1618
+ const retryable = typeof stated.retryable === "boolean" ? stated.retryable : status >= 500;
1619
+ const nextStep = typeof stated.next_step === "string" ? stated.next_step : void 0;
1620
+ return new AgentresError(status, code, message, retryable, nextStep);
1589
1621
  }
1590
- var requestIdCounter = Math.floor(Math.random() * 1e6);
1591
- var sessionCache = /* @__PURE__ */ new Map();
1592
- function generateRequestId() {
1593
- return ++requestIdCounter;
1622
+ function statedError(body) {
1623
+ let parsed;
1624
+ try {
1625
+ parsed = JSON.parse(body);
1626
+ } catch {
1627
+ return null;
1628
+ }
1629
+ const envelope = parsed;
1630
+ const stated = typeof envelope?.error === "object" && envelope.error !== null ? envelope.error : null;
1631
+ if (!stated) {
1632
+ return null;
1633
+ }
1634
+ const fields = stated;
1635
+ if (typeof fields.code !== "string" && typeof fields.message !== "string") {
1636
+ return null;
1637
+ }
1638
+ return fields;
1594
1639
  }
1595
- function parseSSEResponse(text) {
1596
- const events = text.split(/\n\n+/);
1597
- let lastEventData = "";
1598
- for (const event of events) {
1599
- const dataLines = [];
1600
- for (const line of event.split("\n")) {
1601
- if (line.startsWith("data: ")) {
1602
- dataLines.push(line.slice(6));
1603
- } else if (line.startsWith("data:")) {
1604
- dataLines.push(line.slice(5));
1605
- }
1606
- }
1607
- if (dataLines.length > 0) {
1608
- lastEventData = dataLines.join("\n");
1609
- }
1640
+
1641
+ // src/lib/processors/encoding.ts
1642
+ var B64_ENCODE_CHUNK = 32768 * 3;
1643
+ function uint8ArrayToBase64(data) {
1644
+ if (typeof Buffer !== "undefined") {
1645
+ return Buffer.from(data).toString("base64");
1610
1646
  }
1611
- if (!lastEventData) {
1612
- throw new Error("No data found in SSE response");
1647
+ let base64 = "";
1648
+ for (let i = 0; i < data.length; i += B64_ENCODE_CHUNK) {
1649
+ const slice = data.subarray(i, i + B64_ENCODE_CHUNK);
1650
+ let binary = "";
1651
+ for (let j = 0; j < slice.length; j++) {
1652
+ binary += String.fromCharCode(slice[j]);
1653
+ }
1654
+ base64 += btoa(binary);
1613
1655
  }
1614
- return JSON.parse(lastEventData);
1656
+ return base64;
1615
1657
  }
1616
- async function parseResponseBody(response) {
1617
- const contentType = response.headers.get("Content-Type") || "";
1618
- const text = await response.text();
1619
- if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
1620
- return parseSSEResponse(text);
1658
+ function base64ToUint8Array(base64) {
1659
+ if (typeof Buffer !== "undefined") {
1660
+ const buf = Buffer.from(base64, "base64");
1661
+ return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
1621
1662
  }
1622
- return JSON.parse(text);
1663
+ const binary = atob(base64);
1664
+ const bytes = new Uint8Array(binary.length);
1665
+ for (let i = 0; i < binary.length; i++) {
1666
+ bytes[i] = binary.charCodeAt(i);
1667
+ }
1668
+ return bytes;
1623
1669
  }
1624
- async function initializeMCPSession(accessToken) {
1625
- const requestId = generateRequestId();
1626
- const initRequest = {
1627
- jsonrpc: "2.0",
1628
- id: requestId,
1629
- method: "initialize",
1630
- params: {
1631
- protocolVersion: "2024-11-05",
1632
- capabilities: {},
1633
- clientInfo: {
1634
- name: "Anuma",
1635
- version: "1.0.0"
1636
- }
1637
- }
1670
+
1671
+ // src/lib/connectors/agentres/siwx.ts
1672
+ var PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
1673
+ var SIWX_HEADER = "SIGN-IN-WITH-X";
1674
+ var SIWX_EXTENSION = "sign-in-with-x";
1675
+ var SOLANA_NAMESPACE = "solana:";
1676
+ var ED25519_SIGNATURE_BYTES = 64;
1677
+ function parseChallenge(paymentRequiredHeader, expectedOrigin) {
1678
+ const decoded = decodeHeader(paymentRequiredHeader);
1679
+ const extensions = asRecord(decoded.extensions);
1680
+ const extension = extensions ? asRecord(extensions[SIWX_EXTENSION]) : null;
1681
+ if (!extension) {
1682
+ throw new SiwxUnsupportedError(
1683
+ "the 402 carries no sign-in-with-x extension, so this endpoint is paid rather than identity-only"
1684
+ );
1685
+ }
1686
+ const info = asRecord(extension.info);
1687
+ if (!info) {
1688
+ throw new SiwxChallengeError("the sign-in-with-x extension has no info block");
1689
+ }
1690
+ const chain = solanaChain(extension.supportedChains);
1691
+ const challenge = {
1692
+ domain: requireString(info, "domain"),
1693
+ uri: requireString(info, "uri"),
1694
+ version: requireString(info, "version"),
1695
+ statement: requireString(info, "statement"),
1696
+ nonce: requireString(info, "nonce"),
1697
+ issuedAt: requireString(info, "issuedAt"),
1698
+ resources: requireResources(info),
1699
+ chainId: chain.chainId,
1700
+ signingType: chain.signingType
1638
1701
  };
1639
- const response = await fetch(MCP_HTTP_ENDPOINT, {
1640
- method: "POST",
1641
- headers: {
1642
- "Content-Type": "application/json",
1643
- Accept: "application/json, text/event-stream",
1644
- Authorization: `Bearer ${accessToken}`
1645
- },
1646
- body: JSON.stringify(initRequest)
1647
- });
1648
- if (!response.ok) {
1649
- const errorBody = await response.text().catch(() => "");
1650
- throw new Error(
1651
- `MCP initialization failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
1702
+ assertScopedTo(challenge, expectedOrigin);
1703
+ return challenge;
1704
+ }
1705
+ function assertScopedTo(challenge, expectedOrigin) {
1706
+ const expected = parseUrl(expectedOrigin, `the agentres base URL "${expectedOrigin}"`);
1707
+ if (challenge.domain !== expected.host) {
1708
+ throw new SiwxChallengeError(
1709
+ `the challenge is scoped to "${challenge.domain}" but this client talks to "${expected.host}" \u2014 refusing to sign it`
1652
1710
  );
1653
1711
  }
1654
- const sessionId = response.headers.get("Mcp-Session-Id");
1655
- if (!sessionId) {
1656
- throw new Error("No Mcp-Session-Id returned from initialization");
1712
+ const uri = parseUrl(challenge.uri, `the sign-in-with-x uri "${challenge.uri}"`);
1713
+ if (uri.origin !== expected.origin) {
1714
+ throw new SiwxChallengeError(
1715
+ `the challenge addresses "${uri.origin}" but this client talks to "${expected.origin}" \u2014 refusing to sign it`
1716
+ );
1657
1717
  }
1658
- const jsonRpcResponse = await parseResponseBody(response);
1659
- if (jsonRpcResponse.error) {
1660
- const err = jsonRpcResponse.error;
1661
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1662
- throw new Error(`MCP initialization error: ${errMsg}`);
1718
+ }
1719
+ function parseUrl(value, description) {
1720
+ try {
1721
+ return new URL(value);
1722
+ } catch (cause) {
1723
+ throw new SiwxChallengeError(`${description} is not a URL`, { cause });
1663
1724
  }
1664
- await fetch(MCP_HTTP_ENDPOINT, {
1665
- method: "POST",
1666
- headers: {
1667
- "Content-Type": "application/json",
1668
- Authorization: `Bearer ${accessToken}`,
1669
- "Mcp-Session-Id": sessionId
1670
- },
1671
- body: JSON.stringify({
1672
- jsonrpc: "2.0",
1673
- method: "notifications/initialized"
1674
- })
1675
- });
1676
- sessionCache.set(accessToken, sessionId);
1677
- return sessionId;
1678
1725
  }
1679
- async function ensureMCPSession(accessToken) {
1680
- const cached = sessionCache.get(accessToken);
1681
- if (cached) {
1682
- return cached;
1726
+ function buildMessage(challenge, address) {
1727
+ if (!challenge.chainId.startsWith(SOLANA_NAMESPACE)) {
1728
+ throw new SiwxChallengeError(
1729
+ `expected a solana: chain id for the message, got "${challenge.chainId}"`
1730
+ );
1683
1731
  }
1684
- return initializeMCPSession(accessToken);
1732
+ const reference = challenge.chainId.slice(SOLANA_NAMESPACE.length);
1733
+ const resources = challenge.resources.map((resource) => `- ${resource}`).join("\n");
1734
+ return `${challenge.domain} wants you to sign in with your Solana account:
1735
+ ${address}
1736
+
1737
+ ${challenge.statement}
1738
+
1739
+ URI: ${challenge.uri}
1740
+ Version: ${challenge.version}
1741
+ Chain ID: ${reference}
1742
+ Nonce: ${challenge.nonce}
1743
+ Issued At: ${challenge.issuedAt}
1744
+ Resources:
1745
+ ${resources}`;
1685
1746
  }
1686
- async function callMCPTool(accessToken, toolName, args) {
1687
- const sessionId = await ensureMCPSession(accessToken);
1688
- const requestId = generateRequestId();
1689
- const jsonRpcRequest = {
1690
- jsonrpc: "2.0",
1691
- id: requestId,
1692
- method: "tools/call",
1693
- params: {
1694
- name: toolName,
1695
- arguments: args
1696
- }
1747
+ function buildPayload(challenge, address, signature) {
1748
+ if (signature.length !== ED25519_SIGNATURE_BYTES) {
1749
+ throw new SiwxChallengeError(
1750
+ `expected a ${ED25519_SIGNATURE_BYTES}-byte ed25519 signature, got ${signature.length} bytes`
1751
+ );
1752
+ }
1753
+ const payload = {
1754
+ domain: challenge.domain,
1755
+ address,
1756
+ statement: challenge.statement,
1757
+ uri: challenge.uri,
1758
+ version: challenge.version,
1759
+ // Full CAIP-2 here, unlike the bare reference the message carries.
1760
+ chainId: challenge.chainId,
1761
+ type: challenge.signingType,
1762
+ nonce: challenge.nonce,
1763
+ issuedAt: challenge.issuedAt,
1764
+ resources: challenge.resources,
1765
+ signature: base58Encode(signature)
1697
1766
  };
1698
- const response = await fetch(MCP_HTTP_ENDPOINT, {
1699
- method: "POST",
1700
- headers: {
1701
- "Content-Type": "application/json",
1702
- Accept: "application/json, text/event-stream",
1703
- Authorization: `Bearer ${accessToken}`,
1704
- "Mcp-Session-Id": sessionId
1705
- },
1706
- body: JSON.stringify(jsonRpcRequest)
1707
- });
1708
- if (!response.ok) {
1709
- if (response.status === 400 || response.status === 401) {
1710
- sessionCache.delete(accessToken);
1711
- const newSessionId = await initializeMCPSession(accessToken);
1712
- const retryResponse = await fetch(MCP_HTTP_ENDPOINT, {
1713
- method: "POST",
1714
- headers: {
1715
- "Content-Type": "application/json",
1716
- Accept: "application/json, text/event-stream",
1717
- Authorization: `Bearer ${accessToken}`,
1718
- "Mcp-Session-Id": newSessionId
1719
- },
1720
- body: JSON.stringify(jsonRpcRequest)
1721
- });
1722
- if (!retryResponse.ok) {
1723
- const errorBody2 = await retryResponse.text().catch(() => "");
1724
- throw new Error(
1725
- `MCP request failed: ${retryResponse.status} ${retryResponse.statusText}${errorBody2 ? ` - ${errorBody2}` : ""}`
1726
- );
1727
- }
1728
- const retryJsonRpcResponse = await parseResponseBody(retryResponse);
1729
- if (retryJsonRpcResponse.error) {
1730
- const err = retryJsonRpcResponse.error;
1731
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1732
- throw new Error(`MCP tool error: ${errMsg}`);
1733
- }
1734
- return truncateToolResult(retryJsonRpcResponse.result);
1767
+ return uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload)));
1768
+ }
1769
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1770
+ function base58Encode(bytes) {
1771
+ let zeros = 0;
1772
+ while (zeros < bytes.length && bytes[zeros] === 0) {
1773
+ zeros++;
1774
+ }
1775
+ const digits = [];
1776
+ for (let i = zeros; i < bytes.length; i++) {
1777
+ let carry = bytes[i];
1778
+ for (let j = 0; j < digits.length; j++) {
1779
+ carry += digits[j] * 256;
1780
+ digits[j] = carry % 58;
1781
+ carry = Math.floor(carry / 58);
1782
+ }
1783
+ while (carry > 0) {
1784
+ digits.push(carry % 58);
1785
+ carry = Math.floor(carry / 58);
1735
1786
  }
1736
- const errorBody = await response.text().catch(() => "");
1737
- throw new Error(
1738
- `MCP request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
1739
- );
1740
1787
  }
1741
- const jsonRpcResponse = await parseResponseBody(response);
1742
- if (jsonRpcResponse.error) {
1743
- const err = jsonRpcResponse.error;
1744
- const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
1745
- throw new Error(`MCP tool error: ${errMsg}`);
1788
+ let encoded = "1".repeat(zeros);
1789
+ for (let i = digits.length - 1; i >= 0; i--) {
1790
+ encoded += BASE58_ALPHABET[digits[i]];
1746
1791
  }
1747
- return truncateToolResult(jsonRpcResponse.result);
1792
+ return encoded;
1748
1793
  }
1749
- function createNotionSearchTool(getAccessToken, requestNotionAccess) {
1794
+ function decodeHeader(paymentRequiredHeader) {
1795
+ let parsed;
1796
+ try {
1797
+ parsed = JSON.parse(new TextDecoder().decode(base64ToUint8Array(paymentRequiredHeader)));
1798
+ } catch (cause) {
1799
+ throw new SiwxChallengeError("the PAYMENT-REQUIRED header is not base64-encoded JSON", {
1800
+ cause
1801
+ });
1802
+ }
1803
+ const decoded = asRecord(parsed);
1804
+ if (!decoded) {
1805
+ throw new SiwxChallengeError("the PAYMENT-REQUIRED header did not decode to an object");
1806
+ }
1807
+ return decoded;
1808
+ }
1809
+ function solanaChain(supportedChains) {
1810
+ if (!Array.isArray(supportedChains)) {
1811
+ throw new SiwxChallengeError("the sign-in-with-x extension lists no supportedChains");
1812
+ }
1813
+ for (const entry of supportedChains) {
1814
+ const chain = asRecord(entry);
1815
+ const chainId = chain?.chainId;
1816
+ if (typeof chainId !== "string" || !chainId.startsWith(SOLANA_NAMESPACE)) {
1817
+ continue;
1818
+ }
1819
+ const signingType = chain?.type;
1820
+ if (typeof signingType !== "string" || signingType === "") {
1821
+ throw new SiwxChallengeError(`the ${chainId} entry declares no signing type`);
1822
+ }
1823
+ return { chainId, signingType };
1824
+ }
1825
+ throw new SiwxChallengeError("the sign-in-with-x extension offers no solana: chain");
1826
+ }
1827
+ function asRecord(value) {
1828
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1829
+ }
1830
+ function requireString(info, key) {
1831
+ const value = info[key];
1832
+ if (typeof value !== "string" || value === "") {
1833
+ throw new SiwxChallengeError(`the sign-in-with-x info block has no ${key}`);
1834
+ }
1835
+ return value;
1836
+ }
1837
+ function requireResources(info) {
1838
+ const resources = info.resources;
1839
+ if (!Array.isArray(resources) || resources.length === 0) {
1840
+ throw new SiwxChallengeError("the sign-in-with-x info block has no resources");
1841
+ }
1842
+ if (!resources.every((resource) => typeof resource === "string" && resource !== "")) {
1843
+ throw new SiwxChallengeError("the sign-in-with-x resources are not all strings");
1844
+ }
1845
+ return resources;
1846
+ }
1847
+
1848
+ // src/lib/connectors/agentres/client.ts
1849
+ var DEFAULT_BASE_URL = "https://agentres.dev";
1850
+ function createAgentresClient(options) {
1851
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1852
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
1853
+ const { address, signMessage } = options;
1854
+ async function withSiwx(request) {
1855
+ const url = resolveUrl(baseUrl, request.path);
1856
+ const init = requestInit(request);
1857
+ const challenged = await fetchImpl(url, init);
1858
+ if (challenged.status !== 402) {
1859
+ return readBody(challenged);
1860
+ }
1861
+ await challenged.body?.cancel().catch(() => {
1862
+ });
1863
+ const header = challenged.headers.get(PAYMENT_REQUIRED_HEADER);
1864
+ if (!header) {
1865
+ throw new SiwxChallengeError(
1866
+ `the 402 from ${request.path} carried no ${PAYMENT_REQUIRED_HEADER} header`
1867
+ );
1868
+ }
1869
+ const challenge = parseChallenge(header, baseUrl);
1870
+ const signature = await signMessage(new TextEncoder().encode(buildMessage(challenge, address)));
1871
+ const authorized = await fetchImpl(url, {
1872
+ ...init,
1873
+ headers: { ...init.headers, [SIWX_HEADER]: buildPayload(challenge, address, signature) }
1874
+ });
1875
+ return readBody(authorized);
1876
+ }
1877
+ async function linkResy(body) {
1878
+ const response = await withSiwx({ method: "POST", path: "/api/link-resy", body });
1879
+ return { step: response.step, message: response.message, resyUserId: response.resy_user_id };
1880
+ }
1750
1881
  return {
1751
- type: "function",
1752
- function: {
1753
- name: "notion-search",
1754
- description: "Semantic search over Notion workspace and connected sources (Slack, Google Drive, Github, Jira, etc). Can also search for users by name or email.",
1755
- arguments: {
1756
- type: "object",
1757
- properties: {
1758
- query: {
1759
- type: "string",
1760
- description: "Semantic search query over workspace and connected sources. For user search, a substring to match against name or email."
1761
- },
1762
- query_type: {
1763
- type: "string",
1764
- enum: ["internal", "user"],
1765
- description: "Type of search: 'internal' for workspace/content search, 'user' for user search"
1766
- },
1767
- content_search_mode: {
1768
- type: "string",
1769
- enum: ["workspace_search", "ai_search"],
1770
- description: "Override auto-selection of AI search vs workspace search"
1771
- },
1772
- data_source_url: {
1773
- type: "string",
1774
- description: "URL of a data source to search within"
1775
- },
1776
- page_url: {
1777
- type: "string",
1778
- description: "URL or ID of a page to search within"
1779
- },
1780
- teamspace_id: {
1781
- type: "string",
1782
- description: "ID of a teamspace to restrict search results to"
1783
- },
1784
- filters: {
1785
- type: "object",
1786
- description: "Filters for search results (only for query_type 'internal'). Supports created_date_range {start_date, end_date} and created_by_user_ids array."
1787
- }
1788
- },
1789
- required: ["query"]
1790
- }
1882
+ withSiwx,
1883
+ async ensureAccount(email) {
1884
+ const body = await withSiwx({ method: "POST", path: "/api/account", body: { email } });
1885
+ return {
1886
+ accountId: body.account_id,
1887
+ email: body.email,
1888
+ credentialKind: body.credential_kind,
1889
+ walletLinked: body.wallet_linked
1890
+ };
1791
1891
  },
1792
- executor: async (args) => {
1793
- let token = getAccessToken();
1794
- if (!token) {
1795
- token = await requestNotionAccess();
1796
- }
1797
- try {
1798
- return await callMCPTool(token, "notion-search", args);
1799
- } catch (error) {
1800
- return `Error searching Notion: ${error instanceof Error ? error.message : String(error)}`;
1801
- }
1892
+ requestCode(email) {
1893
+ return linkResy({ em_address: email });
1894
+ },
1895
+ verifyCode(email, code) {
1896
+ return linkResy({ em_address: email, code });
1897
+ },
1898
+ async getLinkStatus() {
1899
+ const body = await withSiwx({ method: "GET", path: "/api/me" });
1900
+ return {
1901
+ accountId: body.account_id,
1902
+ email: body.email,
1903
+ resyLinked: body.resy_linked,
1904
+ walletAddress: body.wallet_address,
1905
+ resyLinkedAt: body.resy_linked_at ?? null
1906
+ };
1802
1907
  }
1803
1908
  };
1804
1909
  }
1805
- function createNotionFetchTool(getAccessToken, requestNotionAccess) {
1806
- return {
1807
- type: "function",
1808
- function: {
1809
- name: "notion-fetch",
1810
- description: "Retrieve a Notion page or database by URL or ID. Pages return enhanced Markdown. Databases return all data sources.",
1811
- arguments: {
1812
- type: "object",
1813
- properties: {
1814
- id: {
1815
- type: "string",
1816
- description: "The ID or URL of the Notion page/database. Supports notion.so URLs, Notion Sites URLs (*.notion.site), and raw UUIDs."
1817
- },
1818
- include_transcript: {
1819
- type: "boolean",
1820
- description: "Include transcript if available"
1821
- },
1822
- include_discussions: {
1823
- type: "boolean",
1824
- description: "Include discussion counts and inline discussion markers"
1825
- }
1826
- },
1827
- required: ["id"]
1828
- }
1829
- },
1830
- executor: async (args) => {
1831
- let token = getAccessToken();
1832
- if (!token) {
1833
- token = await requestNotionAccess();
1834
- }
1835
- try {
1836
- return await callMCPTool(token, "notion-fetch", args);
1837
- } catch (error) {
1838
- return `Error fetching Notion page: ${error instanceof Error ? error.message : String(error)}`;
1839
- }
1840
- }
1841
- };
1842
- }
1843
- function createNotionCreatePagesTool(getAccessToken, requestNotionAccess) {
1844
- return {
1845
- type: "function",
1846
- function: {
1847
- name: "notion-create-pages",
1848
- 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.",
1849
- arguments: {
1850
- type: "object",
1851
- properties: {
1852
- pages: {
1853
- type: "array",
1854
- description: "Array of page objects to create (max 100)",
1855
- items: {
1856
- type: "object",
1857
- properties: {
1858
- properties: {
1859
- type: "object",
1860
- 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."
1861
- },
1862
- content: {
1863
- type: "string",
1864
- description: "Page content in Notion-flavored Markdown format"
1865
- }
1866
- },
1867
- required: ["properties", "content"]
1868
- }
1869
- },
1870
- parent: {
1871
- type: "object",
1872
- 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."
1873
- }
1874
- },
1875
- required: ["pages"]
1876
- }
1877
- },
1878
- executor: async (args) => {
1879
- let token = getAccessToken();
1880
- if (!token) {
1881
- token = await requestNotionAccess();
1882
- }
1883
- try {
1884
- return await callMCPTool(token, "notion-create-pages", args);
1885
- } catch (error) {
1886
- return `Error creating Notion page: ${error instanceof Error ? error.message : String(error)}`;
1887
- }
1888
- }
1889
- };
1890
- }
1891
- function createNotionUpdatePageTool(getAccessToken, requestNotionAccess) {
1892
- return {
1893
- type: "function",
1894
- function: {
1895
- name: "notion-update-page",
1896
- 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.",
1897
- arguments: {
1898
- type: "object",
1899
- properties: {
1900
- data: {
1901
- type: "object",
1902
- description: "Update data containing page_id and a command",
1903
- properties: {
1904
- page_id: {
1905
- type: "string",
1906
- description: "The ID of the page to update (with or without dashes)"
1907
- },
1908
- command: {
1909
- type: "string",
1910
- enum: [
1911
- "update_properties",
1912
- "replace_content",
1913
- "replace_content_range",
1914
- "insert_content_after"
1915
- ],
1916
- description: "The update command to execute"
1917
- },
1918
- properties: {
1919
- type: "object",
1920
- description: "For update_properties: JSON map of property names to values. Use null to remove a value."
1921
- },
1922
- new_str: {
1923
- type: "string",
1924
- description: "For replace_content/replace_content_range/insert_content_after: the new content string"
1925
- },
1926
- selection_with_ellipsis: {
1927
- type: "string",
1928
- description: "For replace_content_range/insert_content_after: unique start and end snippet (~10 chars each with ellipsis)"
1929
- },
1930
- allow_deleting_content: {
1931
- type: "boolean",
1932
- description: "For replace_content/replace_content_range: allow deletion of child pages/databases"
1933
- }
1934
- },
1935
- required: ["page_id", "command"]
1936
- }
1937
- },
1938
- required: ["data"]
1939
- }
1940
- },
1941
- executor: async (args) => {
1942
- let token = getAccessToken();
1943
- if (!token) {
1944
- token = await requestNotionAccess();
1945
- }
1946
- try {
1947
- return await callMCPTool(token, "notion-update-page", args);
1948
- } catch (error) {
1949
- return `Error updating Notion page: ${error instanceof Error ? error.message : String(error)}`;
1950
- }
1951
- }
1952
- };
1910
+ function resolveUrl(baseUrl, path) {
1911
+ if (!path.startsWith("/") || path.startsWith("//")) {
1912
+ throw new AgentresPathError(
1913
+ `"${path}" is not a path on ${baseUrl} \u2014 it must start with a single "/"`
1914
+ );
1915
+ }
1916
+ const base = new URL(baseUrl);
1917
+ const resolved = new URL(path, base);
1918
+ if (resolved.origin !== base.origin) {
1919
+ throw new AgentresPathError(
1920
+ `"${path}" resolves to ${resolved.origin} but this client talks to ${base.origin} \u2014 refusing to send it`
1921
+ );
1922
+ }
1923
+ return resolved.toString();
1953
1924
  }
1954
- function createNotionMovePagesTool(getAccessToken, requestNotionAccess) {
1955
- return {
1956
- type: "function",
1957
- function: {
1958
- name: "notion-move-pages",
1959
- description: "Move one or more Notion pages or databases to a new parent.",
1960
- arguments: {
1961
- type: "object",
1962
- properties: {
1963
- page_or_database_ids: {
1964
- type: "array",
1965
- description: "Array of page or database IDs to move (UUIDs, max 100)",
1966
- items: { type: "string" }
1967
- },
1968
- new_parent: {
1969
- type: "object",
1970
- 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."
1971
- }
1972
- },
1973
- required: ["page_or_database_ids", "new_parent"]
1974
- }
1975
- },
1976
- executor: async (args) => {
1977
- let token = getAccessToken();
1978
- if (!token) {
1979
- token = await requestNotionAccess();
1980
- }
1981
- try {
1982
- return await callMCPTool(token, "notion-move-pages", args);
1983
- } catch (error) {
1984
- return `Error moving Notion pages: ${error instanceof Error ? error.message : String(error)}`;
1985
- }
1986
- }
1987
- };
1925
+ function requestInit(request) {
1926
+ const headers = { Accept: "application/json" };
1927
+ if (request.body === void 0) {
1928
+ return { method: request.method, headers };
1929
+ }
1930
+ headers["Content-Type"] = "application/json";
1931
+ return { method: request.method, headers, body: JSON.stringify(request.body) };
1988
1932
  }
1989
- function createNotionDuplicatePageTool(getAccessToken, requestNotionAccess) {
1990
- return {
1991
- type: "function",
1992
- function: {
1993
- name: "notion-duplicate-page",
1994
- description: "Duplicate a Notion page. The duplication completes asynchronously.",
1995
- arguments: {
1996
- type: "object",
1997
- properties: {
1998
- page_id: {
1999
- type: "string",
2000
- description: "The ID of the page to duplicate (UUID with or without dashes)"
2001
- }
2002
- },
2003
- required: ["page_id"]
2004
- }
2005
- },
2006
- executor: async (args) => {
2007
- let token = getAccessToken();
2008
- if (!token) {
2009
- token = await requestNotionAccess();
2010
- }
2011
- try {
2012
- return await callMCPTool(token, "notion-duplicate-page", args);
2013
- } catch (error) {
2014
- return `Error duplicating Notion page: ${error instanceof Error ? error.message : String(error)}`;
2015
- }
2016
- }
2017
- };
1933
+ async function readBody(response) {
1934
+ const body = await response.text();
1935
+ if (!response.ok) {
1936
+ throw parseAgentresError(response.status, body);
1937
+ }
1938
+ try {
1939
+ return JSON.parse(body);
1940
+ } catch {
1941
+ throw new AgentresError(
1942
+ response.status,
1943
+ "INVALID_JSON",
1944
+ `agentres answered ${response.status} with a body that is not JSON`,
1945
+ response.status >= 500
1946
+ );
1947
+ }
2018
1948
  }
2019
- function createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess) {
2020
- return {
2021
- type: "function",
2022
- function: {
2023
- name: "notion-create-database",
2024
- 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.",
2025
- arguments: {
2026
- type: "object",
2027
- properties: {
2028
- properties: {
2029
- type: "object",
2030
- description: "Property schema for the database. Each key is a property name, value defines the type."
2031
- },
2032
- parent: {
2033
- type: "object",
2034
- description: "Parent page: {type:'page_id', page_id:'...'}. Omit for private workspace-level database."
2035
- },
2036
- title: {
2037
- type: "array",
2038
- description: "Title of the database as rich text array (max 100)",
2039
- items: { type: "object" }
2040
- },
2041
- description: {
2042
- type: "array",
2043
- description: "Description of the database as rich text array (max 100)",
2044
- items: { type: "object" }
2045
- }
2046
- },
2047
- required: ["properties"]
2048
- }
2049
- },
2050
- executor: async (args) => {
2051
- let token = getAccessToken();
2052
- if (!token) {
2053
- token = await requestNotionAccess();
2054
- }
2055
- try {
2056
- return await callMCPTool(token, "notion-create-database", args);
2057
- } catch (error) {
2058
- return `Error creating Notion database: ${error instanceof Error ? error.message : String(error)}`;
2059
- }
1949
+
1950
+ // src/lib/connectors/client.ts
1951
+ var DEFAULT_REFRESH_BEFORE_MS = 3e4;
1952
+ function createConnectorTokenGetter(client, provider, opts = {}) {
1953
+ const refreshBeforeMs = opts.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
1954
+ const now = opts.now ?? Date.now;
1955
+ let cached = null;
1956
+ return async () => {
1957
+ if (cached && cached.expiresAt - refreshBeforeMs > now()) {
1958
+ return cached.accessToken;
2060
1959
  }
2061
- };
2062
- }
2063
- function createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess) {
2064
- return {
2065
- type: "function",
2066
- function: {
2067
- name: "notion-update-data-source",
2068
- 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.",
2069
- arguments: {
2070
- type: "object",
2071
- properties: {
2072
- data_source_id: {
2073
- type: "string",
2074
- description: "The ID of the data source to update (UUID). Can be a data source ID or database ID."
2075
- },
2076
- title: {
2077
- type: "array",
2078
- description: "New title as rich text array (max 100)",
2079
- items: { type: "object" }
2080
- },
2081
- description: {
2082
- type: "array",
2083
- description: "New description as rich text array (max 100)",
2084
- items: { type: "object" }
2085
- },
2086
- properties: {
2087
- type: "object",
2088
- description: "Property schema updates. Use null to remove, {name:'...'} to rename, or full definition to add/update."
2089
- },
2090
- is_inline: {
2091
- type: "boolean",
2092
- description: "Whether the database is inline"
2093
- },
2094
- in_trash: {
2095
- type: "boolean",
2096
- description: "Trash the data source (requires Notion UI to undo)"
2097
- }
2098
- },
2099
- required: ["data_source_id"]
2100
- }
2101
- },
2102
- executor: async (args) => {
2103
- let token = getAccessToken();
2104
- if (!token) {
2105
- token = await requestNotionAccess();
2106
- }
2107
- try {
2108
- return await callMCPTool(token, "notion-update-data-source", args);
2109
- } catch (error) {
2110
- return `Error updating Notion data source: ${error instanceof Error ? error.message : String(error)}`;
2111
- }
1960
+ let result;
1961
+ try {
1962
+ result = await client.mintConnectorToken(provider);
1963
+ } catch {
1964
+ cached = null;
1965
+ return null;
2112
1966
  }
2113
- };
2114
- }
2115
- function createNotionCreateCommentTool(getAccessToken, requestNotionAccess) {
2116
- return {
2117
- type: "function",
2118
- function: {
2119
- name: "notion-create-comment",
2120
- 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).",
2121
- arguments: {
2122
- type: "object",
2123
- properties: {
2124
- page_id: {
2125
- type: "string",
2126
- description: "The ID of the page to comment on (with or without dashes)"
2127
- },
2128
- rich_text: {
2129
- type: "array",
2130
- description: "Comment content as rich text array (max 100). Objects can be text, mention, or equation.",
2131
- items: {
2132
- type: "object",
2133
- properties: {
2134
- text: {
2135
- type: "object",
2136
- properties: {
2137
- content: { type: "string" }
2138
- },
2139
- required: ["content"]
2140
- }
2141
- },
2142
- required: ["text"]
2143
- }
2144
- },
2145
- discussion_id: {
2146
- type: "string",
2147
- description: "ID or URL of an existing discussion to reply to"
2148
- },
2149
- selection_with_ellipsis: {
2150
- type: "string",
2151
- description: "Unique start and end snippet (~10 chars each with ellipsis) of content to comment on"
2152
- }
2153
- },
2154
- required: ["page_id", "rich_text"]
2155
- }
2156
- },
2157
- executor: async (args) => {
2158
- let token = getAccessToken();
2159
- if (!token) {
2160
- token = await requestNotionAccess();
2161
- }
2162
- try {
2163
- return await callMCPTool(token, "notion-create-comment", args);
2164
- } catch (error) {
2165
- return `Error creating Notion comment: ${error instanceof Error ? error.message : String(error)}`;
2166
- }
1967
+ if (result.ok) {
1968
+ cached = { accessToken: result.accessToken, expiresAt: result.expiresAt };
1969
+ return result.accessToken;
2167
1970
  }
2168
- };
2169
- }
2170
- function createNotionGetCommentsTool(getAccessToken, requestNotionAccess) {
2171
- return {
2172
- type: "function",
2173
- function: {
2174
- name: "notion-get-comments",
2175
- description: "Get comments and discussions from a Notion page. Returns discussions with full comment content.",
2176
- arguments: {
2177
- type: "object",
2178
- properties: {
2179
- page_id: {
2180
- type: "string",
2181
- description: "Identifier for the Notion page"
2182
- },
2183
- include_all_blocks: {
2184
- type: "boolean",
2185
- description: "Include discussions on child blocks (default: false)"
2186
- },
2187
- include_resolved: {
2188
- type: "boolean",
2189
- description: "Include resolved discussions (default: false)"
2190
- },
2191
- discussion_id: {
2192
- type: "string",
2193
- description: "Fetch a specific discussion by ID or URL (e.g., discussion://pageId/blockId/discussionId)"
2194
- }
2195
- },
2196
- required: ["page_id"]
2197
- }
2198
- },
2199
- executor: async (args) => {
2200
- let token = getAccessToken();
2201
- if (!token) {
2202
- token = await requestNotionAccess();
2203
- }
2204
- try {
2205
- return await callMCPTool(token, "notion-get-comments", args);
2206
- } catch (error) {
2207
- return `Error retrieving Notion comments: ${error instanceof Error ? error.message : String(error)}`;
2208
- }
1971
+ cached = null;
1972
+ if (opts.onNotConnected) {
1973
+ return opts.onNotConnected(result.error);
2209
1974
  }
1975
+ return null;
2210
1976
  };
2211
1977
  }
2212
- function createNotionGetUsersTool(getAccessToken, requestNotionAccess) {
2213
- return {
2214
- type: "function",
2215
- function: {
2216
- name: "notion-get-users",
2217
- 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').",
2218
- arguments: {
2219
- type: "object",
2220
- properties: {
2221
- query: {
2222
- type: "string",
2223
- description: "Search query to filter users by name or email (case-insensitive, max 100 chars)"
2224
- },
2225
- start_cursor: {
2226
- type: "string",
2227
- description: "Cursor for pagination from previous response"
2228
- },
2229
- page_size: {
2230
- type: "integer",
2231
- description: "Number of users per page (1-100, default 100)"
2232
- },
2233
- user_id: {
2234
- type: "string",
2235
- description: "Return only the user matching this ID. Pass 'self' to fetch the current bot user."
2236
- }
2237
- }
2238
- }
2239
- },
2240
- executor: async (args) => {
2241
- let token = getAccessToken();
2242
- if (!token) {
2243
- token = await requestNotionAccess();
2244
- }
2245
- try {
2246
- return await callMCPTool(token, "notion-get-users", args);
2247
- } catch (error) {
2248
- return `Error listing Notion users: ${error instanceof Error ? error.message : String(error)}`;
1978
+
1979
+ // src/tools/notion.ts
1980
+ var MCP_HTTP_ENDPOINT = "https://mcp.notion.com/mcp";
1981
+ var MCP_SSE_ENDPOINT = "https://mcp.notion.com/sse";
1982
+ var MAX_CONTENT_LENGTH2 = 5e4;
1983
+ function truncateToolResult(result) {
1984
+ if (result === void 0 || result === null) return result;
1985
+ const stringified = typeof result === "string" ? result : JSON.stringify(result);
1986
+ if (stringified.length <= MAX_CONTENT_LENGTH2) {
1987
+ return result;
1988
+ }
1989
+ return stringified.slice(0, MAX_CONTENT_LENGTH2) + `
1990
+
1991
+ ... (content truncated, showing first ${MAX_CONTENT_LENGTH2} characters of ${stringified.length})`;
1992
+ }
1993
+ var requestIdCounter = Math.floor(Math.random() * 1e6);
1994
+ var sessionCache = /* @__PURE__ */ new Map();
1995
+ function generateRequestId() {
1996
+ return ++requestIdCounter;
1997
+ }
1998
+ function parseSSEResponse(text) {
1999
+ const events = text.split(/\n\n+/);
2000
+ let lastEventData = "";
2001
+ for (const event of events) {
2002
+ const dataLines = [];
2003
+ for (const line of event.split("\n")) {
2004
+ if (line.startsWith("data: ")) {
2005
+ dataLines.push(line.slice(6));
2006
+ } else if (line.startsWith("data:")) {
2007
+ dataLines.push(line.slice(5));
2249
2008
  }
2250
2009
  }
2251
- };
2010
+ if (dataLines.length > 0) {
2011
+ lastEventData = dataLines.join("\n");
2012
+ }
2013
+ }
2014
+ if (!lastEventData) {
2015
+ throw new Error("No data found in SSE response");
2016
+ }
2017
+ return JSON.parse(lastEventData);
2252
2018
  }
2253
- function createNotionGetTeamsTool(getAccessToken, requestNotionAccess) {
2254
- return {
2255
- type: "function",
2256
- function: {
2257
- name: "notion-get-teams",
2258
- description: "Retrieve teams (teamspaces) in the Notion workspace. Shows team names, IDs, membership status, and roles.",
2259
- arguments: {
2260
- type: "object",
2261
- properties: {
2262
- query: {
2263
- type: "string",
2264
- description: "Search query to filter teams by name (case-insensitive, max 100 chars)"
2265
- }
2266
- }
2267
- }
2268
- },
2269
- executor: async (args) => {
2270
- let token = getAccessToken();
2271
- if (!token) {
2272
- token = await requestNotionAccess();
2273
- }
2274
- try {
2275
- return await callMCPTool(token, "notion-get-teams", args);
2276
- } catch (error) {
2277
- return `Error retrieving Notion teams: ${error instanceof Error ? error.message : String(error)}`;
2019
+ async function parseResponseBody(response) {
2020
+ const contentType = response.headers.get("Content-Type") || "";
2021
+ const text = await response.text();
2022
+ if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
2023
+ return parseSSEResponse(text);
2024
+ }
2025
+ return JSON.parse(text);
2026
+ }
2027
+ async function initializeMCPSession(accessToken) {
2028
+ const requestId = generateRequestId();
2029
+ const initRequest = {
2030
+ jsonrpc: "2.0",
2031
+ id: requestId,
2032
+ method: "initialize",
2033
+ params: {
2034
+ protocolVersion: "2024-11-05",
2035
+ capabilities: {},
2036
+ clientInfo: {
2037
+ name: "Anuma",
2038
+ version: "1.0.0"
2278
2039
  }
2279
2040
  }
2280
2041
  };
2042
+ const response = await fetch(MCP_HTTP_ENDPOINT, {
2043
+ method: "POST",
2044
+ headers: {
2045
+ "Content-Type": "application/json",
2046
+ Accept: "application/json, text/event-stream",
2047
+ Authorization: `Bearer ${accessToken}`
2048
+ },
2049
+ body: JSON.stringify(initRequest)
2050
+ });
2051
+ if (!response.ok) {
2052
+ const errorBody = await response.text().catch(() => "");
2053
+ throw new Error(
2054
+ `MCP initialization failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
2055
+ );
2056
+ }
2057
+ const sessionId = response.headers.get("Mcp-Session-Id");
2058
+ if (!sessionId) {
2059
+ throw new Error("No Mcp-Session-Id returned from initialization");
2060
+ }
2061
+ const jsonRpcResponse = await parseResponseBody(response);
2062
+ if (jsonRpcResponse.error) {
2063
+ const err = jsonRpcResponse.error;
2064
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2065
+ throw new Error(`MCP initialization error: ${errMsg}`);
2066
+ }
2067
+ await fetch(MCP_HTTP_ENDPOINT, {
2068
+ method: "POST",
2069
+ headers: {
2070
+ "Content-Type": "application/json",
2071
+ Authorization: `Bearer ${accessToken}`,
2072
+ "Mcp-Session-Id": sessionId
2073
+ },
2074
+ body: JSON.stringify({
2075
+ jsonrpc: "2.0",
2076
+ method: "notifications/initialized"
2077
+ })
2078
+ });
2079
+ sessionCache.set(accessToken, sessionId);
2080
+ return sessionId;
2281
2081
  }
2282
- function createNotionTools(getAccessToken, requestNotionAccess) {
2283
- return [
2284
- // Search
2285
- createNotionSearchTool(getAccessToken, requestNotionAccess),
2286
- // Pages
2287
- createNotionFetchTool(getAccessToken, requestNotionAccess),
2288
- createNotionCreatePagesTool(getAccessToken, requestNotionAccess),
2289
- createNotionUpdatePageTool(getAccessToken, requestNotionAccess),
2290
- createNotionMovePagesTool(getAccessToken, requestNotionAccess),
2291
- createNotionDuplicatePageTool(getAccessToken, requestNotionAccess),
2292
- // Data Sources (Databases)
2293
- createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess),
2294
- createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess),
2295
- // Comments
2296
- createNotionCreateCommentTool(getAccessToken, requestNotionAccess),
2297
- createNotionGetCommentsTool(getAccessToken, requestNotionAccess),
2298
- // Users & Teams
2299
- createNotionGetUsersTool(getAccessToken, requestNotionAccess),
2300
- createNotionGetTeamsTool(getAccessToken, requestNotionAccess)
2301
- ];
2082
+ async function ensureMCPSession(accessToken) {
2083
+ const cached = sessionCache.get(accessToken);
2084
+ if (cached) {
2085
+ return cached;
2086
+ }
2087
+ return initializeMCPSession(accessToken);
2302
2088
  }
2303
- function getMCPEndpoints() {
2304
- return {
2305
- http: MCP_HTTP_ENDPOINT,
2306
- sse: MCP_SSE_ENDPOINT
2089
+ async function callMCPTool(accessToken, toolName, args) {
2090
+ const sessionId = await ensureMCPSession(accessToken);
2091
+ const requestId = generateRequestId();
2092
+ const jsonRpcRequest = {
2093
+ jsonrpc: "2.0",
2094
+ id: requestId,
2095
+ method: "tools/call",
2096
+ params: {
2097
+ name: toolName,
2098
+ arguments: args
2099
+ }
2307
2100
  };
2308
- }
2309
- async function callNotionMCPTool(accessToken, toolName, args) {
2310
- return callMCPTool(accessToken, toolName, args);
2311
- }
2312
-
2313
- // src/tools/github.ts
2314
- var GITHUB_API = "https://api.github.com";
2315
- var FETCH_TIMEOUT_MS = 3e4;
2316
- async function resolveToken(getAccessToken, requestGitHubAccess) {
2317
- let token = getAccessToken();
2318
- if (!token) {
2319
- try {
2320
- token = await requestGitHubAccess();
2321
- } catch (err) {
2322
- getLogger().error("Failed to resolve GitHub token", err);
2323
- return null;
2101
+ const response = await fetch(MCP_HTTP_ENDPOINT, {
2102
+ method: "POST",
2103
+ headers: {
2104
+ "Content-Type": "application/json",
2105
+ Accept: "application/json, text/event-stream",
2106
+ Authorization: `Bearer ${accessToken}`,
2107
+ "Mcp-Session-Id": sessionId
2108
+ },
2109
+ body: JSON.stringify(jsonRpcRequest)
2110
+ });
2111
+ if (!response.ok) {
2112
+ if (response.status === 400 || response.status === 401) {
2113
+ sessionCache.delete(accessToken);
2114
+ const newSessionId = await initializeMCPSession(accessToken);
2115
+ const retryResponse = await fetch(MCP_HTTP_ENDPOINT, {
2116
+ method: "POST",
2117
+ headers: {
2118
+ "Content-Type": "application/json",
2119
+ Accept: "application/json, text/event-stream",
2120
+ Authorization: `Bearer ${accessToken}`,
2121
+ "Mcp-Session-Id": newSessionId
2122
+ },
2123
+ body: JSON.stringify(jsonRpcRequest)
2124
+ });
2125
+ if (!retryResponse.ok) {
2126
+ const errorBody2 = await retryResponse.text().catch(() => "");
2127
+ throw new Error(
2128
+ `MCP request failed: ${retryResponse.status} ${retryResponse.statusText}${errorBody2 ? ` - ${errorBody2}` : ""}`
2129
+ );
2130
+ }
2131
+ const retryJsonRpcResponse = await parseResponseBody(retryResponse);
2132
+ if (retryJsonRpcResponse.error) {
2133
+ const err = retryJsonRpcResponse.error;
2134
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2135
+ throw new Error(`MCP tool error: ${errMsg}`);
2136
+ }
2137
+ return truncateToolResult(retryJsonRpcResponse.result);
2324
2138
  }
2139
+ const errorBody = await response.text().catch(() => "");
2140
+ throw new Error(
2141
+ `MCP request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
2142
+ );
2325
2143
  }
2326
- return token;
2327
- }
2328
- async function githubFetch(token, path, options = {}) {
2329
- if (!path.startsWith("http") && !path.startsWith("/")) {
2330
- throw new Error("githubFetch: relative path must start with /");
2144
+ const jsonRpcResponse = await parseResponseBody(response);
2145
+ if (jsonRpcResponse.error) {
2146
+ const err = jsonRpcResponse.error;
2147
+ const errMsg = typeof err.message === "string" ? err.message : JSON.stringify(err);
2148
+ throw new Error(`MCP tool error: ${errMsg}`);
2331
2149
  }
2332
- const url = path.startsWith("http") ? path : `${GITHUB_API}${path}`;
2333
- if (!url.startsWith(`${GITHUB_API}/`)) {
2334
- throw new Error("githubFetch: URL must target GitHub API");
2150
+ return truncateToolResult(jsonRpcResponse.result);
2151
+ }
2152
+ var NOTION_PROVIDER = "notion";
2153
+ function directRunner(getAccessToken, requestNotionAccess) {
2154
+ return async (toolName, args) => {
2155
+ let token = getAccessToken();
2156
+ if (!token) {
2157
+ token = await requestNotionAccess();
2158
+ }
2159
+ return callMCPTool(token, toolName, args);
2160
+ };
2161
+ }
2162
+ function readField(json, key) {
2163
+ return typeof json === "object" && json !== null ? json[key] : void 0;
2164
+ }
2165
+ function readString(json, key) {
2166
+ const value = readField(json, key);
2167
+ return typeof value === "string" ? value : void 0;
2168
+ }
2169
+ function notionConnectorError(status, json) {
2170
+ const code = readField(json, "code");
2171
+ if (status === 503) {
2172
+ return code === "upstream_unavailable" ? buildConnectorErrorResult("upstream_unavailable", NOTION_PROVIDER) : null;
2173
+ }
2174
+ if (status !== 401 && status !== 403 && status !== 412) return null;
2175
+ switch (code) {
2176
+ case "scope_not_covered": {
2177
+ const missingScopes = readField(json, "missing_scopes");
2178
+ return buildConnectorErrorResult(
2179
+ "scope_not_covered",
2180
+ NOTION_PROVIDER,
2181
+ readString(json, "connect_url"),
2182
+ Array.isArray(missingScopes) ? { missingScopes } : void 0
2183
+ );
2184
+ }
2185
+ case "insufficient_scope":
2186
+ return buildConnectorErrorResult("insufficient_scope", NOTION_PROVIDER, void 0, {
2187
+ required: readString(json, "required")
2188
+ });
2189
+ case "upstream_unavailable":
2190
+ return buildConnectorErrorResult("upstream_unavailable", NOTION_PROVIDER);
2191
+ default:
2192
+ return buildConnectorErrorResult(
2193
+ "connector_not_connected",
2194
+ NOTION_PROVIDER,
2195
+ readString(json, "connect_url")
2196
+ );
2335
2197
  }
2336
- const controller = new AbortController();
2337
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2338
- try {
2339
- return await fetch(url, {
2340
- ...options,
2341
- signal: controller.signal,
2342
- headers: {
2343
- Authorization: `Bearer ${token}`,
2344
- Accept: "application/vnd.github+json",
2345
- "Content-Type": "application/json",
2346
- "X-GitHub-Api-Version": "2022-11-28",
2347
- ...options.headers
2198
+ }
2199
+ function proxyRunner(callMcp) {
2200
+ return async (toolName, args) => {
2201
+ const { status, json } = await callMcp(toolName, args);
2202
+ if (status >= 200 && status < 300) {
2203
+ const result = readField(json, "result");
2204
+ if (result === void 0) {
2205
+ throw new Error(`Notion returned no result (${status})`);
2348
2206
  }
2349
- });
2350
- } finally {
2351
- clearTimeout(timeout);
2352
- }
2207
+ return truncateToolResult(result);
2208
+ }
2209
+ const connectorError = notionConnectorError(status, json);
2210
+ if (connectorError) return connectorError;
2211
+ const message = readField(json, "error");
2212
+ throw new Error(`${typeof message === "string" ? message : JSON.stringify(json)} (${status})`);
2213
+ };
2353
2214
  }
2354
- function createGetAuthenticatedUserTool(getAccessToken, requestAccess) {
2215
+ function withNotionExecutor(run, errorPrefix, tool) {
2355
2216
  return {
2356
- type: "function",
2357
- function: {
2358
- name: "github_get_authenticated_user",
2359
- 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.",
2360
- parameters: {
2361
- type: "object",
2362
- properties: {}
2363
- }
2364
- },
2365
- executor: async () => {
2366
- const token = await resolveToken(getAccessToken, requestAccess);
2367
- if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2217
+ ...tool,
2218
+ executor: async (args) => {
2368
2219
  try {
2369
- const userResp = await githubFetch(token, "/user");
2370
- if (!userResp.ok) return `Error: Failed to get user (${userResp.status})`;
2371
- const user = await userResp.json();
2372
- const orgsResp = await githubFetch(token, "/user/orgs?per_page=100");
2373
- let orgs = [];
2374
- if (orgsResp.ok) {
2375
- const orgsData = await orgsResp.json();
2376
- orgs = orgsData.map((o) => o.login);
2377
- }
2378
- return {
2379
- login: user.login,
2380
- name: user.name,
2381
- email: user.email,
2382
- organizations: orgs,
2383
- public_repos: user.public_repos,
2384
- html_url: user.html_url
2385
- };
2386
- } catch (err) {
2387
- getLogger().error("github_get_authenticated_user error", err);
2388
- return "Error: Failed to get user profile.";
2220
+ return await run(tool.function.name, args);
2221
+ } catch (error) {
2222
+ return `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`;
2389
2223
  }
2390
2224
  }
2391
2225
  };
2392
2226
  }
2393
- var MAX_RESPONSE_SIZE = 1e5;
2394
- function truncate(text, maxLen) {
2395
- if (text.length <= maxLen) return text;
2396
- return text.slice(0, maxLen) + `
2397
-
2398
- ... (truncated, ${text.length - maxLen} characters omitted)`;
2399
- }
2400
- function createGitHubApiTool(getAccessToken, requestAccess) {
2401
- return {
2227
+ function notionSearchTool(run) {
2228
+ return withNotionExecutor(run, "Error searching Notion", {
2402
2229
  type: "function",
2403
2230
  function: {
2404
- name: "github_api",
2405
- 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.
2406
-
2407
- Examples:
2408
- - List PRs: GET /repos/{owner}/{repo}/pulls?state=open
2409
- - Get file: GET /repos/{owner}/{repo}/contents/{path}
2410
- - Create issue: POST /repos/{owner}/{repo}/issues with body {"title": "...", "body": "..."}
2411
- - Search repos: GET /search/repositories?q={query}
2412
-
2413
- 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.`,
2414
- parameters: {
2231
+ name: "notion-search",
2232
+ description: "Semantic search over Notion workspace and connected sources (Slack, Google Drive, Github, Jira, etc). Can also search for users by name or email.",
2233
+ arguments: {
2415
2234
  type: "object",
2416
2235
  properties: {
2417
- method: {
2236
+ query: {
2418
2237
  type: "string",
2419
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
2420
- description: "HTTP method"
2238
+ description: "Semantic search query over workspace and connected sources. For user search, a substring to match against name or email."
2421
2239
  },
2422
- path: {
2240
+ query_type: {
2423
2241
  type: "string",
2424
- description: "API path (e.g., /repos/owner/repo/pulls) or full URL for pagination"
2242
+ enum: ["internal", "user"],
2243
+ description: "Type of search: 'internal' for workspace/content search, 'user' for user search"
2425
2244
  },
2426
- body: {
2245
+ content_search_mode: {
2246
+ type: "string",
2247
+ enum: ["workspace_search", "ai_search"],
2248
+ description: "Override auto-selection of AI search vs workspace search"
2249
+ },
2250
+ data_source_url: {
2251
+ type: "string",
2252
+ description: "URL of a data source to search within"
2253
+ },
2254
+ page_url: {
2255
+ type: "string",
2256
+ description: "URL or ID of a page to search within"
2257
+ },
2258
+ teamspace_id: {
2259
+ type: "string",
2260
+ description: "ID of a teamspace to restrict search results to"
2261
+ },
2262
+ filters: {
2427
2263
  type: "object",
2428
- description: "Request body for POST/PUT/PATCH requests",
2429
- additionalProperties: true
2264
+ description: "Filters for search results (only for query_type 'internal'). Supports created_date_range {start_date, end_date} and created_by_user_ids array."
2430
2265
  }
2431
2266
  },
2432
- required: ["method", "path"]
2433
- }
2434
- },
2435
- executor: async (args) => {
2436
- const token = await resolveToken(getAccessToken, requestAccess);
2437
- if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2438
- const method = args.method;
2439
- const path = args.path;
2440
- const body = args.body;
2441
- try {
2442
- const resp = await githubFetch(token, path, {
2443
- method,
2444
- body: body ? JSON.stringify(body) : void 0
2445
- });
2446
- const text = await resp.text();
2447
- if (!resp.ok) {
2448
- return `Error: GitHub API ${method} ${path} returned ${resp.status}: ${truncate(text, 500)}`;
2449
- }
2450
- try {
2451
- const data = JSON.parse(text);
2452
- const json = JSON.stringify(data);
2453
- return truncate(json, MAX_RESPONSE_SIZE);
2454
- } catch {
2455
- return truncate(text, MAX_RESPONSE_SIZE);
2456
- }
2457
- } catch (err) {
2458
- if (err instanceof DOMException && err.name === "AbortError") {
2459
- return `Error: GitHub API request timed out after ${FETCH_TIMEOUT_MS / 1e3}s.`;
2460
- }
2461
- getLogger().error("github_api error", err);
2462
- return `Error: GitHub API request failed: ${err instanceof Error ? err.message : "unknown error"}`;
2267
+ required: ["query"]
2463
2268
  }
2464
2269
  }
2465
- };
2466
- }
2467
- function createGitHubTools(getAccessToken, requestGitHubAccess) {
2468
- return [
2469
- createGetAuthenticatedUserTool(getAccessToken, requestGitHubAccess),
2470
- createGitHubApiTool(getAccessToken, requestGitHubAccess)
2471
- ];
2472
- }
2473
-
2474
- // src/lib/connectors/agentres/errors.ts
2475
- var AgentresError = class extends Error {
2476
- constructor(status, code, message, retryable, nextStep) {
2477
- super(message);
2478
- this.name = "AgentresError";
2479
- this.status = status;
2480
- this.code = code;
2481
- this.retryable = retryable;
2482
- this.nextStep = nextStep;
2483
- }
2484
- };
2485
- var SiwxUnsupportedError = class extends Error {
2486
- constructor(message) {
2487
- super(message);
2488
- this.name = "SiwxUnsupportedError";
2489
- }
2490
- };
2491
- var AgentresPathError = class extends Error {
2492
- constructor(message) {
2493
- super(message);
2494
- this.name = "AgentresPathError";
2495
- }
2496
- };
2497
- var SiwxChallengeError = class extends Error {
2498
- constructor(message, options) {
2499
- super(message);
2500
- this.name = "SiwxChallengeError";
2501
- this.cause = options?.cause;
2502
- }
2503
- };
2504
- function parseAgentresError(status, body) {
2505
- const stated = statedError(body);
2506
- if (!stated) {
2507
- return new AgentresError(
2508
- status,
2509
- `HTTP_${status}`,
2510
- body.trim() || `HTTP ${status}`,
2511
- status >= 500
2512
- );
2513
- }
2514
- const code = typeof stated.code === "string" ? stated.code : `HTTP_${status}`;
2515
- const message = typeof stated.message === "string" ? stated.message : `HTTP ${status}`;
2516
- const retryable = typeof stated.retryable === "boolean" ? stated.retryable : status >= 500;
2517
- const nextStep = typeof stated.next_step === "string" ? stated.next_step : void 0;
2518
- return new AgentresError(status, code, message, retryable, nextStep);
2270
+ });
2519
2271
  }
2520
- function statedError(body) {
2521
- let parsed;
2522
- try {
2523
- parsed = JSON.parse(body);
2524
- } catch {
2525
- return null;
2526
- }
2527
- const envelope = parsed;
2528
- const stated = typeof envelope?.error === "object" && envelope.error !== null ? envelope.error : null;
2529
- if (!stated) {
2530
- return null;
2531
- }
2532
- const fields = stated;
2533
- if (typeof fields.code !== "string" && typeof fields.message !== "string") {
2534
- return null;
2535
- }
2536
- return fields;
2272
+ function createNotionSearchTool(getAccessToken, requestNotionAccess) {
2273
+ return notionSearchTool(directRunner(getAccessToken, requestNotionAccess));
2537
2274
  }
2538
-
2539
- // src/lib/processors/encoding.ts
2540
- var B64_ENCODE_CHUNK = 32768 * 3;
2541
- function uint8ArrayToBase64(data) {
2542
- if (typeof Buffer !== "undefined") {
2543
- return Buffer.from(data).toString("base64");
2544
- }
2545
- let base64 = "";
2546
- for (let i = 0; i < data.length; i += B64_ENCODE_CHUNK) {
2547
- const slice = data.subarray(i, i + B64_ENCODE_CHUNK);
2548
- let binary = "";
2549
- for (let j = 0; j < slice.length; j++) {
2550
- binary += String.fromCharCode(slice[j]);
2275
+ function notionFetchTool(run) {
2276
+ return withNotionExecutor(run, "Error fetching Notion page", {
2277
+ type: "function",
2278
+ function: {
2279
+ name: "notion-fetch",
2280
+ description: "Retrieve a Notion page or database by URL or ID. Pages return enhanced Markdown. Databases return all data sources.",
2281
+ arguments: {
2282
+ type: "object",
2283
+ properties: {
2284
+ id: {
2285
+ type: "string",
2286
+ description: "The ID or URL of the Notion page/database. Supports notion.so URLs, Notion Sites URLs (*.notion.site), and raw UUIDs."
2287
+ },
2288
+ include_transcript: {
2289
+ type: "boolean",
2290
+ description: "Include transcript if available"
2291
+ },
2292
+ include_discussions: {
2293
+ type: "boolean",
2294
+ description: "Include discussion counts and inline discussion markers"
2295
+ }
2296
+ },
2297
+ required: ["id"]
2298
+ }
2551
2299
  }
2552
- base64 += btoa(binary);
2553
- }
2554
- return base64;
2555
- }
2556
- function base64ToUint8Array(base64) {
2557
- if (typeof Buffer !== "undefined") {
2558
- const buf = Buffer.from(base64, "base64");
2559
- return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
2560
- }
2561
- const binary = atob(base64);
2562
- const bytes = new Uint8Array(binary.length);
2563
- for (let i = 0; i < binary.length; i++) {
2564
- bytes[i] = binary.charCodeAt(i);
2565
- }
2566
- return bytes;
2300
+ });
2567
2301
  }
2568
-
2569
- // src/lib/connectors/agentres/siwx.ts
2570
- var PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
2571
- var SIWX_HEADER = "SIGN-IN-WITH-X";
2572
- var SIWX_EXTENSION = "sign-in-with-x";
2573
- var SOLANA_NAMESPACE = "solana:";
2574
- var ED25519_SIGNATURE_BYTES = 64;
2575
- function parseChallenge(paymentRequiredHeader, expectedOrigin) {
2576
- const decoded = decodeHeader(paymentRequiredHeader);
2577
- const extensions = asRecord(decoded.extensions);
2578
- const extension = extensions ? asRecord(extensions[SIWX_EXTENSION]) : null;
2579
- if (!extension) {
2580
- throw new SiwxUnsupportedError(
2581
- "the 402 carries no sign-in-with-x extension, so this endpoint is paid rather than identity-only"
2582
- );
2583
- }
2584
- const info = asRecord(extension.info);
2585
- if (!info) {
2586
- throw new SiwxChallengeError("the sign-in-with-x extension has no info block");
2587
- }
2588
- const chain = solanaChain(extension.supportedChains);
2589
- const challenge = {
2590
- domain: requireString(info, "domain"),
2591
- uri: requireString(info, "uri"),
2592
- version: requireString(info, "version"),
2593
- statement: requireString(info, "statement"),
2594
- nonce: requireString(info, "nonce"),
2595
- issuedAt: requireString(info, "issuedAt"),
2596
- resources: requireResources(info),
2597
- chainId: chain.chainId,
2598
- signingType: chain.signingType
2599
- };
2600
- assertScopedTo(challenge, expectedOrigin);
2601
- return challenge;
2302
+ function createNotionFetchTool(getAccessToken, requestNotionAccess) {
2303
+ return notionFetchTool(directRunner(getAccessToken, requestNotionAccess));
2602
2304
  }
2603
- function assertScopedTo(challenge, expectedOrigin) {
2604
- const expected = parseUrl(expectedOrigin, `the agentres base URL "${expectedOrigin}"`);
2605
- if (challenge.domain !== expected.host) {
2606
- throw new SiwxChallengeError(
2607
- `the challenge is scoped to "${challenge.domain}" but this client talks to "${expected.host}" \u2014 refusing to sign it`
2608
- );
2609
- }
2610
- const uri = parseUrl(challenge.uri, `the sign-in-with-x uri "${challenge.uri}"`);
2611
- if (uri.origin !== expected.origin) {
2612
- throw new SiwxChallengeError(
2613
- `the challenge addresses "${uri.origin}" but this client talks to "${expected.origin}" \u2014 refusing to sign it`
2614
- );
2615
- }
2305
+ function notionCreatePagesTool(run) {
2306
+ return withNotionExecutor(run, "Error creating Notion page", {
2307
+ type: "function",
2308
+ function: {
2309
+ name: "notion-create-pages",
2310
+ 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.",
2311
+ arguments: {
2312
+ type: "object",
2313
+ properties: {
2314
+ pages: {
2315
+ type: "array",
2316
+ description: "Array of page objects to create (max 100)",
2317
+ items: {
2318
+ type: "object",
2319
+ properties: {
2320
+ properties: {
2321
+ type: "object",
2322
+ 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."
2323
+ },
2324
+ content: {
2325
+ type: "string",
2326
+ description: "Page content in Notion-flavored Markdown format"
2327
+ }
2328
+ },
2329
+ required: ["properties", "content"]
2330
+ }
2331
+ },
2332
+ parent: {
2333
+ type: "object",
2334
+ 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."
2335
+ }
2336
+ },
2337
+ required: ["pages"]
2338
+ }
2339
+ }
2340
+ });
2616
2341
  }
2617
- function parseUrl(value, description) {
2618
- try {
2619
- return new URL(value);
2620
- } catch (cause) {
2621
- throw new SiwxChallengeError(`${description} is not a URL`, { cause });
2622
- }
2342
+ function createNotionCreatePagesTool(getAccessToken, requestNotionAccess) {
2343
+ return notionCreatePagesTool(directRunner(getAccessToken, requestNotionAccess));
2623
2344
  }
2624
- function buildMessage(challenge, address) {
2625
- if (!challenge.chainId.startsWith(SOLANA_NAMESPACE)) {
2626
- throw new SiwxChallengeError(
2627
- `expected a solana: chain id for the message, got "${challenge.chainId}"`
2628
- );
2629
- }
2630
- const reference = challenge.chainId.slice(SOLANA_NAMESPACE.length);
2631
- const resources = challenge.resources.map((resource) => `- ${resource}`).join("\n");
2632
- return `${challenge.domain} wants you to sign in with your Solana account:
2633
- ${address}
2634
-
2635
- ${challenge.statement}
2636
-
2637
- URI: ${challenge.uri}
2638
- Version: ${challenge.version}
2639
- Chain ID: ${reference}
2640
- Nonce: ${challenge.nonce}
2641
- Issued At: ${challenge.issuedAt}
2642
- Resources:
2643
- ${resources}`;
2345
+ function notionUpdatePageTool(run) {
2346
+ return withNotionExecutor(run, "Error updating Notion page", {
2347
+ type: "function",
2348
+ function: {
2349
+ name: "notion-update-page",
2350
+ 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.",
2351
+ arguments: {
2352
+ type: "object",
2353
+ properties: {
2354
+ data: {
2355
+ type: "object",
2356
+ description: "Update data containing page_id and a command",
2357
+ properties: {
2358
+ page_id: {
2359
+ type: "string",
2360
+ description: "The ID of the page to update (with or without dashes)"
2361
+ },
2362
+ command: {
2363
+ type: "string",
2364
+ enum: [
2365
+ "update_properties",
2366
+ "replace_content",
2367
+ "replace_content_range",
2368
+ "insert_content_after"
2369
+ ],
2370
+ description: "The update command to execute"
2371
+ },
2372
+ properties: {
2373
+ type: "object",
2374
+ description: "For update_properties: JSON map of property names to values. Use null to remove a value."
2375
+ },
2376
+ new_str: {
2377
+ type: "string",
2378
+ description: "For replace_content/replace_content_range/insert_content_after: the new content string"
2379
+ },
2380
+ selection_with_ellipsis: {
2381
+ type: "string",
2382
+ description: "For replace_content_range/insert_content_after: unique start and end snippet (~10 chars each with ellipsis)"
2383
+ },
2384
+ allow_deleting_content: {
2385
+ type: "boolean",
2386
+ description: "For replace_content/replace_content_range: allow deletion of child pages/databases"
2387
+ }
2388
+ },
2389
+ required: ["page_id", "command"]
2390
+ }
2391
+ },
2392
+ required: ["data"]
2393
+ }
2394
+ }
2395
+ });
2644
2396
  }
2645
- function buildPayload(challenge, address, signature) {
2646
- if (signature.length !== ED25519_SIGNATURE_BYTES) {
2647
- throw new SiwxChallengeError(
2648
- `expected a ${ED25519_SIGNATURE_BYTES}-byte ed25519 signature, got ${signature.length} bytes`
2649
- );
2650
- }
2651
- const payload = {
2652
- domain: challenge.domain,
2653
- address,
2654
- statement: challenge.statement,
2655
- uri: challenge.uri,
2656
- version: challenge.version,
2657
- // Full CAIP-2 here, unlike the bare reference the message carries.
2658
- chainId: challenge.chainId,
2659
- type: challenge.signingType,
2660
- nonce: challenge.nonce,
2661
- issuedAt: challenge.issuedAt,
2662
- resources: challenge.resources,
2663
- signature: base58Encode(signature)
2664
- };
2665
- return uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload)));
2397
+ function createNotionUpdatePageTool(getAccessToken, requestNotionAccess) {
2398
+ return notionUpdatePageTool(directRunner(getAccessToken, requestNotionAccess));
2666
2399
  }
2667
- var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2668
- function base58Encode(bytes) {
2669
- let zeros = 0;
2670
- while (zeros < bytes.length && bytes[zeros] === 0) {
2671
- zeros++;
2672
- }
2673
- const digits = [];
2674
- for (let i = zeros; i < bytes.length; i++) {
2675
- let carry = bytes[i];
2676
- for (let j = 0; j < digits.length; j++) {
2677
- carry += digits[j] * 256;
2678
- digits[j] = carry % 58;
2679
- carry = Math.floor(carry / 58);
2400
+ function notionMovePagesTool(run) {
2401
+ return withNotionExecutor(run, "Error moving Notion pages", {
2402
+ type: "function",
2403
+ function: {
2404
+ name: "notion-move-pages",
2405
+ description: "Move one or more Notion pages or databases to a new parent.",
2406
+ arguments: {
2407
+ type: "object",
2408
+ properties: {
2409
+ page_or_database_ids: {
2410
+ type: "array",
2411
+ description: "Array of page or database IDs to move (UUIDs, max 100)",
2412
+ items: { type: "string" }
2413
+ },
2414
+ new_parent: {
2415
+ type: "object",
2416
+ 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."
2417
+ }
2418
+ },
2419
+ required: ["page_or_database_ids", "new_parent"]
2420
+ }
2680
2421
  }
2681
- while (carry > 0) {
2682
- digits.push(carry % 58);
2683
- carry = Math.floor(carry / 58);
2422
+ });
2423
+ }
2424
+ function createNotionMovePagesTool(getAccessToken, requestNotionAccess) {
2425
+ return notionMovePagesTool(directRunner(getAccessToken, requestNotionAccess));
2426
+ }
2427
+ function notionDuplicatePageTool(run) {
2428
+ return withNotionExecutor(run, "Error duplicating Notion page", {
2429
+ type: "function",
2430
+ function: {
2431
+ name: "notion-duplicate-page",
2432
+ description: "Duplicate a Notion page. The duplication completes asynchronously.",
2433
+ arguments: {
2434
+ type: "object",
2435
+ properties: {
2436
+ page_id: {
2437
+ type: "string",
2438
+ description: "The ID of the page to duplicate (UUID with or without dashes)"
2439
+ }
2440
+ },
2441
+ required: ["page_id"]
2442
+ }
2684
2443
  }
2685
- }
2686
- let encoded = "1".repeat(zeros);
2687
- for (let i = digits.length - 1; i >= 0; i--) {
2688
- encoded += BASE58_ALPHABET[digits[i]];
2689
- }
2690
- return encoded;
2444
+ });
2691
2445
  }
2692
- function decodeHeader(paymentRequiredHeader) {
2693
- let parsed;
2694
- try {
2695
- parsed = JSON.parse(new TextDecoder().decode(base64ToUint8Array(paymentRequiredHeader)));
2696
- } catch (cause) {
2697
- throw new SiwxChallengeError("the PAYMENT-REQUIRED header is not base64-encoded JSON", {
2698
- cause
2699
- });
2700
- }
2701
- const decoded = asRecord(parsed);
2702
- if (!decoded) {
2703
- throw new SiwxChallengeError("the PAYMENT-REQUIRED header did not decode to an object");
2704
- }
2705
- return decoded;
2446
+ function createNotionDuplicatePageTool(getAccessToken, requestNotionAccess) {
2447
+ return notionDuplicatePageTool(directRunner(getAccessToken, requestNotionAccess));
2706
2448
  }
2707
- function solanaChain(supportedChains) {
2708
- if (!Array.isArray(supportedChains)) {
2709
- throw new SiwxChallengeError("the sign-in-with-x extension lists no supportedChains");
2710
- }
2711
- for (const entry of supportedChains) {
2712
- const chain = asRecord(entry);
2713
- const chainId = chain?.chainId;
2714
- if (typeof chainId !== "string" || !chainId.startsWith(SOLANA_NAMESPACE)) {
2715
- continue;
2716
- }
2717
- const signingType = chain?.type;
2718
- if (typeof signingType !== "string" || signingType === "") {
2719
- throw new SiwxChallengeError(`the ${chainId} entry declares no signing type`);
2449
+ function notionCreateDatabaseTool(run) {
2450
+ return withNotionExecutor(run, "Error creating Notion database", {
2451
+ type: "function",
2452
+ function: {
2453
+ name: "notion-create-database",
2454
+ 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.",
2455
+ arguments: {
2456
+ type: "object",
2457
+ properties: {
2458
+ properties: {
2459
+ type: "object",
2460
+ description: "Property schema for the database. Each key is a property name, value defines the type."
2461
+ },
2462
+ parent: {
2463
+ type: "object",
2464
+ description: "Parent page: {type:'page_id', page_id:'...'}. Omit for private workspace-level database."
2465
+ },
2466
+ title: {
2467
+ type: "array",
2468
+ description: "Title of the database as rich text array (max 100)",
2469
+ items: { type: "object" }
2470
+ },
2471
+ description: {
2472
+ type: "array",
2473
+ description: "Description of the database as rich text array (max 100)",
2474
+ items: { type: "object" }
2475
+ }
2476
+ },
2477
+ required: ["properties"]
2478
+ }
2720
2479
  }
2721
- return { chainId, signingType };
2722
- }
2723
- throw new SiwxChallengeError("the sign-in-with-x extension offers no solana: chain");
2480
+ });
2724
2481
  }
2725
- function asRecord(value) {
2726
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2482
+ function createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess) {
2483
+ return notionCreateDatabaseTool(directRunner(getAccessToken, requestNotionAccess));
2727
2484
  }
2728
- function requireString(info, key) {
2729
- const value = info[key];
2730
- if (typeof value !== "string" || value === "") {
2731
- throw new SiwxChallengeError(`the sign-in-with-x info block has no ${key}`);
2732
- }
2733
- return value;
2485
+ function notionUpdateDataSourceTool(run) {
2486
+ return withNotionExecutor(run, "Error updating Notion data source", {
2487
+ type: "function",
2488
+ function: {
2489
+ name: "notion-update-data-source",
2490
+ 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.",
2491
+ arguments: {
2492
+ type: "object",
2493
+ properties: {
2494
+ data_source_id: {
2495
+ type: "string",
2496
+ description: "The ID of the data source to update (UUID). Can be a data source ID or database ID."
2497
+ },
2498
+ title: {
2499
+ type: "array",
2500
+ description: "New title as rich text array (max 100)",
2501
+ items: { type: "object" }
2502
+ },
2503
+ description: {
2504
+ type: "array",
2505
+ description: "New description as rich text array (max 100)",
2506
+ items: { type: "object" }
2507
+ },
2508
+ properties: {
2509
+ type: "object",
2510
+ description: "Property schema updates. Use null to remove, {name:'...'} to rename, or full definition to add/update."
2511
+ },
2512
+ is_inline: {
2513
+ type: "boolean",
2514
+ description: "Whether the database is inline"
2515
+ },
2516
+ in_trash: {
2517
+ type: "boolean",
2518
+ description: "Trash the data source (requires Notion UI to undo)"
2519
+ }
2520
+ },
2521
+ required: ["data_source_id"]
2522
+ }
2523
+ }
2524
+ });
2734
2525
  }
2735
- function requireResources(info) {
2736
- const resources = info.resources;
2737
- if (!Array.isArray(resources) || resources.length === 0) {
2738
- throw new SiwxChallengeError("the sign-in-with-x info block has no resources");
2739
- }
2740
- if (!resources.every((resource) => typeof resource === "string" && resource !== "")) {
2741
- throw new SiwxChallengeError("the sign-in-with-x resources are not all strings");
2742
- }
2743
- return resources;
2526
+ function createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess) {
2527
+ return notionUpdateDataSourceTool(directRunner(getAccessToken, requestNotionAccess));
2744
2528
  }
2745
-
2746
- // src/lib/connectors/agentres/client.ts
2747
- var DEFAULT_BASE_URL = "https://agentres.dev";
2748
- function createAgentresClient(options) {
2749
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
2750
- const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
2751
- const { address, signMessage } = options;
2752
- async function withSiwx(request) {
2753
- const url = resolveUrl(baseUrl, request.path);
2754
- const init = requestInit(request);
2755
- const challenged = await fetchImpl(url, init);
2756
- if (challenged.status !== 402) {
2757
- return readBody(challenged);
2529
+ function notionCreateCommentTool(run) {
2530
+ return withNotionExecutor(run, "Error creating Notion comment", {
2531
+ type: "function",
2532
+ function: {
2533
+ name: "notion-create-comment",
2534
+ 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).",
2535
+ arguments: {
2536
+ type: "object",
2537
+ properties: {
2538
+ page_id: {
2539
+ type: "string",
2540
+ description: "The ID of the page to comment on (with or without dashes)"
2541
+ },
2542
+ rich_text: {
2543
+ type: "array",
2544
+ description: "Comment content as rich text array (max 100). Objects can be text, mention, or equation.",
2545
+ items: {
2546
+ type: "object",
2547
+ properties: {
2548
+ text: {
2549
+ type: "object",
2550
+ properties: {
2551
+ content: { type: "string" }
2552
+ },
2553
+ required: ["content"]
2554
+ }
2555
+ },
2556
+ required: ["text"]
2557
+ }
2558
+ },
2559
+ discussion_id: {
2560
+ type: "string",
2561
+ description: "ID or URL of an existing discussion to reply to"
2562
+ },
2563
+ selection_with_ellipsis: {
2564
+ type: "string",
2565
+ description: "Unique start and end snippet (~10 chars each with ellipsis) of content to comment on"
2566
+ }
2567
+ },
2568
+ required: ["page_id", "rich_text"]
2569
+ }
2758
2570
  }
2759
- await challenged.body?.cancel().catch(() => {
2760
- });
2761
- const header = challenged.headers.get(PAYMENT_REQUIRED_HEADER);
2762
- if (!header) {
2763
- throw new SiwxChallengeError(
2764
- `the 402 from ${request.path} carried no ${PAYMENT_REQUIRED_HEADER} header`
2765
- );
2571
+ });
2572
+ }
2573
+ function createNotionCreateCommentTool(getAccessToken, requestNotionAccess) {
2574
+ return notionCreateCommentTool(directRunner(getAccessToken, requestNotionAccess));
2575
+ }
2576
+ function notionGetCommentsTool(run) {
2577
+ return withNotionExecutor(run, "Error retrieving Notion comments", {
2578
+ type: "function",
2579
+ function: {
2580
+ name: "notion-get-comments",
2581
+ description: "Get comments and discussions from a Notion page. Returns discussions with full comment content.",
2582
+ arguments: {
2583
+ type: "object",
2584
+ properties: {
2585
+ page_id: {
2586
+ type: "string",
2587
+ description: "Identifier for the Notion page"
2588
+ },
2589
+ include_all_blocks: {
2590
+ type: "boolean",
2591
+ description: "Include discussions on child blocks (default: false)"
2592
+ },
2593
+ include_resolved: {
2594
+ type: "boolean",
2595
+ description: "Include resolved discussions (default: false)"
2596
+ },
2597
+ discussion_id: {
2598
+ type: "string",
2599
+ description: "Fetch a specific discussion by ID or URL (e.g., discussion://pageId/blockId/discussionId)"
2600
+ }
2601
+ },
2602
+ required: ["page_id"]
2603
+ }
2766
2604
  }
2767
- const challenge = parseChallenge(header, baseUrl);
2768
- const signature = await signMessage(new TextEncoder().encode(buildMessage(challenge, address)));
2769
- const authorized = await fetchImpl(url, {
2770
- ...init,
2771
- headers: { ...init.headers, [SIWX_HEADER]: buildPayload(challenge, address, signature) }
2772
- });
2773
- return readBody(authorized);
2774
- }
2775
- async function linkResy(body) {
2776
- const response = await withSiwx({ method: "POST", path: "/api/link-resy", body });
2777
- return { step: response.step, message: response.message, resyUserId: response.resy_user_id };
2778
- }
2779
- return {
2780
- withSiwx,
2781
- async ensureAccount(email) {
2782
- const body = await withSiwx({ method: "POST", path: "/api/account", body: { email } });
2783
- return {
2784
- accountId: body.account_id,
2785
- email: body.email,
2786
- credentialKind: body.credential_kind,
2787
- walletLinked: body.wallet_linked
2788
- };
2789
- },
2790
- requestCode(email) {
2791
- return linkResy({ em_address: email });
2792
- },
2793
- verifyCode(email, code) {
2794
- return linkResy({ em_address: email, code });
2795
- },
2796
- async getLinkStatus() {
2797
- const body = await withSiwx({ method: "GET", path: "/api/me" });
2798
- return {
2799
- accountId: body.account_id,
2800
- email: body.email,
2801
- resyLinked: body.resy_linked,
2802
- walletAddress: body.wallet_address,
2803
- resyLinkedAt: body.resy_linked_at ?? null
2804
- };
2605
+ });
2606
+ }
2607
+ function createNotionGetCommentsTool(getAccessToken, requestNotionAccess) {
2608
+ return notionGetCommentsTool(directRunner(getAccessToken, requestNotionAccess));
2609
+ }
2610
+ function notionGetUsersTool(run) {
2611
+ return withNotionExecutor(run, "Error listing Notion users", {
2612
+ type: "function",
2613
+ function: {
2614
+ name: "notion-get-users",
2615
+ 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').",
2616
+ arguments: {
2617
+ type: "object",
2618
+ properties: {
2619
+ query: {
2620
+ type: "string",
2621
+ description: "Search query to filter users by name or email (case-insensitive, max 100 chars)"
2622
+ },
2623
+ start_cursor: {
2624
+ type: "string",
2625
+ description: "Cursor for pagination from previous response"
2626
+ },
2627
+ page_size: {
2628
+ type: "integer",
2629
+ description: "Number of users per page (1-100, default 100)"
2630
+ },
2631
+ user_id: {
2632
+ type: "string",
2633
+ description: "Return only the user matching this ID. Pass 'self' to fetch the current bot user."
2634
+ }
2635
+ }
2636
+ }
2637
+ }
2638
+ });
2639
+ }
2640
+ function createNotionGetUsersTool(getAccessToken, requestNotionAccess) {
2641
+ return notionGetUsersTool(directRunner(getAccessToken, requestNotionAccess));
2642
+ }
2643
+ function notionGetTeamsTool(run) {
2644
+ return withNotionExecutor(run, "Error retrieving Notion teams", {
2645
+ type: "function",
2646
+ function: {
2647
+ name: "notion-get-teams",
2648
+ description: "Retrieve teams (teamspaces) in the Notion workspace. Shows team names, IDs, membership status, and roles.",
2649
+ arguments: {
2650
+ type: "object",
2651
+ properties: {
2652
+ query: {
2653
+ type: "string",
2654
+ description: "Search query to filter teams by name (case-insensitive, max 100 chars)"
2655
+ }
2656
+ }
2657
+ }
2805
2658
  }
2659
+ });
2660
+ }
2661
+ function createNotionGetTeamsTool(getAccessToken, requestNotionAccess) {
2662
+ return notionGetTeamsTool(directRunner(getAccessToken, requestNotionAccess));
2663
+ }
2664
+ var NOTION_TOOLS = [
2665
+ // Search
2666
+ notionSearchTool,
2667
+ // Pages
2668
+ notionFetchTool,
2669
+ notionCreatePagesTool,
2670
+ notionUpdatePageTool,
2671
+ notionMovePagesTool,
2672
+ notionDuplicatePageTool,
2673
+ // Data Sources (Databases)
2674
+ notionCreateDatabaseTool,
2675
+ notionUpdateDataSourceTool,
2676
+ // Comments
2677
+ notionCreateCommentTool,
2678
+ notionGetCommentsTool,
2679
+ // Users & Teams
2680
+ notionGetUsersTool,
2681
+ notionGetTeamsTool
2682
+ ];
2683
+ function createNotionTools(getAccessToken, requestNotionAccess) {
2684
+ const run = directRunner(getAccessToken, requestNotionAccess);
2685
+ return NOTION_TOOLS.map((tool) => tool(run));
2686
+ }
2687
+ function createNotionProxyTools(callMcp) {
2688
+ const run = proxyRunner(callMcp);
2689
+ return NOTION_TOOLS.map((tool) => tool(run));
2690
+ }
2691
+ function getMCPEndpoints() {
2692
+ return {
2693
+ http: MCP_HTTP_ENDPOINT,
2694
+ sse: MCP_SSE_ENDPOINT
2806
2695
  };
2807
2696
  }
2808
- function resolveUrl(baseUrl, path) {
2809
- if (!path.startsWith("/") || path.startsWith("//")) {
2810
- throw new AgentresPathError(
2811
- `"${path}" is not a path on ${baseUrl} \u2014 it must start with a single "/"`
2812
- );
2813
- }
2814
- const base = new URL(baseUrl);
2815
- const resolved = new URL(path, base);
2816
- if (resolved.origin !== base.origin) {
2817
- throw new AgentresPathError(
2818
- `"${path}" resolves to ${resolved.origin} but this client talks to ${base.origin} \u2014 refusing to send it`
2819
- );
2820
- }
2821
- return resolved.toString();
2697
+ async function callNotionMCPTool(accessToken, toolName, args) {
2698
+ return callMCPTool(accessToken, toolName, args);
2822
2699
  }
2823
- function requestInit(request) {
2824
- const headers = { Accept: "application/json" };
2825
- if (request.body === void 0) {
2826
- return { method: request.method, headers };
2700
+
2701
+ // src/tools/github.ts
2702
+ var GITHUB_API = "https://api.github.com";
2703
+ var FETCH_TIMEOUT_MS = 3e4;
2704
+ async function resolveToken(getAccessToken, requestGitHubAccess) {
2705
+ let token = getAccessToken();
2706
+ if (!token) {
2707
+ try {
2708
+ token = await requestGitHubAccess();
2709
+ } catch (err) {
2710
+ getLogger().error("Failed to resolve GitHub token", err);
2711
+ return null;
2712
+ }
2827
2713
  }
2828
- headers["Content-Type"] = "application/json";
2829
- return { method: request.method, headers, body: JSON.stringify(request.body) };
2714
+ return token;
2830
2715
  }
2831
- async function readBody(response) {
2832
- const body = await response.text();
2833
- if (!response.ok) {
2834
- throw parseAgentresError(response.status, body);
2716
+ async function githubFetch(token, path, options = {}) {
2717
+ if (!path.startsWith("http") && !path.startsWith("/")) {
2718
+ throw new Error("githubFetch: relative path must start with /");
2719
+ }
2720
+ const url = path.startsWith("http") ? path : `${GITHUB_API}${path}`;
2721
+ if (!url.startsWith(`${GITHUB_API}/`)) {
2722
+ throw new Error("githubFetch: URL must target GitHub API");
2835
2723
  }
2724
+ const controller = new AbortController();
2725
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2836
2726
  try {
2837
- return JSON.parse(body);
2838
- } catch {
2839
- throw new AgentresError(
2840
- response.status,
2841
- "INVALID_JSON",
2842
- `agentres answered ${response.status} with a body that is not JSON`,
2843
- response.status >= 500
2844
- );
2727
+ return await fetch(url, {
2728
+ ...options,
2729
+ signal: controller.signal,
2730
+ headers: {
2731
+ Authorization: `Bearer ${token}`,
2732
+ Accept: "application/vnd.github+json",
2733
+ "Content-Type": "application/json",
2734
+ "X-GitHub-Api-Version": "2022-11-28",
2735
+ ...options.headers
2736
+ }
2737
+ });
2738
+ } finally {
2739
+ clearTimeout(timeout);
2845
2740
  }
2846
2741
  }
2847
-
2848
- // src/lib/connectors/client.ts
2849
- var DEFAULT_REFRESH_BEFORE_MS = 3e4;
2850
- function createConnectorTokenGetter(client, provider, opts = {}) {
2851
- const refreshBeforeMs = opts.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
2852
- const now = opts.now ?? Date.now;
2853
- let cached = null;
2854
- return async () => {
2855
- if (cached && cached.expiresAt - refreshBeforeMs > now()) {
2856
- return cached.accessToken;
2857
- }
2858
- let result;
2859
- try {
2860
- result = await client.mintConnectorToken(provider);
2861
- } catch {
2862
- cached = null;
2863
- return null;
2864
- }
2865
- if (result.ok) {
2866
- cached = { accessToken: result.accessToken, expiresAt: result.expiresAt };
2867
- return result.accessToken;
2742
+ function createGetAuthenticatedUserTool(getAccessToken, requestAccess) {
2743
+ return {
2744
+ type: "function",
2745
+ function: {
2746
+ name: "github_get_authenticated_user",
2747
+ 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.",
2748
+ parameters: {
2749
+ type: "object",
2750
+ properties: {}
2751
+ }
2752
+ },
2753
+ executor: async () => {
2754
+ const token = await resolveToken(getAccessToken, requestAccess);
2755
+ if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2756
+ try {
2757
+ const userResp = await githubFetch(token, "/user");
2758
+ if (!userResp.ok) return `Error: Failed to get user (${userResp.status})`;
2759
+ const user = await userResp.json();
2760
+ const orgsResp = await githubFetch(token, "/user/orgs?per_page=100");
2761
+ let orgs = [];
2762
+ if (orgsResp.ok) {
2763
+ const orgsData = await orgsResp.json();
2764
+ orgs = orgsData.map((o) => o.login);
2765
+ }
2766
+ return {
2767
+ login: user.login,
2768
+ name: user.name,
2769
+ email: user.email,
2770
+ organizations: orgs,
2771
+ public_repos: user.public_repos,
2772
+ html_url: user.html_url
2773
+ };
2774
+ } catch (err) {
2775
+ getLogger().error("github_get_authenticated_user error", err);
2776
+ return "Error: Failed to get user profile.";
2777
+ }
2868
2778
  }
2869
- cached = null;
2870
- if (opts.onNotConnected) {
2871
- return opts.onNotConnected(result.error);
2779
+ };
2780
+ }
2781
+ var MAX_RESPONSE_SIZE = 1e5;
2782
+ function truncate(text, maxLen) {
2783
+ if (text.length <= maxLen) return text;
2784
+ return text.slice(0, maxLen) + `
2785
+
2786
+ ... (truncated, ${text.length - maxLen} characters omitted)`;
2787
+ }
2788
+ function createGitHubApiTool(getAccessToken, requestAccess) {
2789
+ return {
2790
+ type: "function",
2791
+ function: {
2792
+ name: "github_api",
2793
+ 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.
2794
+
2795
+ Examples:
2796
+ - List PRs: GET /repos/{owner}/{repo}/pulls?state=open
2797
+ - Get file: GET /repos/{owner}/{repo}/contents/{path}
2798
+ - Create issue: POST /repos/{owner}/{repo}/issues with body {"title": "...", "body": "..."}
2799
+ - Search repos: GET /search/repositories?q={query}
2800
+
2801
+ 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.`,
2802
+ parameters: {
2803
+ type: "object",
2804
+ properties: {
2805
+ method: {
2806
+ type: "string",
2807
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
2808
+ description: "HTTP method"
2809
+ },
2810
+ path: {
2811
+ type: "string",
2812
+ description: "API path (e.g., /repos/owner/repo/pulls) or full URL for pagination"
2813
+ },
2814
+ body: {
2815
+ type: "object",
2816
+ description: "Request body for POST/PUT/PATCH requests",
2817
+ additionalProperties: true
2818
+ }
2819
+ },
2820
+ required: ["method", "path"]
2821
+ }
2822
+ },
2823
+ executor: async (args) => {
2824
+ const token = await resolveToken(getAccessToken, requestAccess);
2825
+ if (!token) return "Error: GitHub not connected. Please connect your GitHub account first.";
2826
+ const method = args.method;
2827
+ const path = args.path;
2828
+ const body = args.body;
2829
+ try {
2830
+ const resp = await githubFetch(token, path, {
2831
+ method,
2832
+ body: body ? JSON.stringify(body) : void 0
2833
+ });
2834
+ const text = await resp.text();
2835
+ if (!resp.ok) {
2836
+ return `Error: GitHub API ${method} ${path} returned ${resp.status}: ${truncate(text, 500)}`;
2837
+ }
2838
+ try {
2839
+ const data = JSON.parse(text);
2840
+ const json = JSON.stringify(data);
2841
+ return truncate(json, MAX_RESPONSE_SIZE);
2842
+ } catch {
2843
+ return truncate(text, MAX_RESPONSE_SIZE);
2844
+ }
2845
+ } catch (err) {
2846
+ if (err instanceof DOMException && err.name === "AbortError") {
2847
+ return `Error: GitHub API request timed out after ${FETCH_TIMEOUT_MS / 1e3}s.`;
2848
+ }
2849
+ getLogger().error("github_api error", err);
2850
+ return `Error: GitHub API request failed: ${err instanceof Error ? err.message : "unknown error"}`;
2851
+ }
2872
2852
  }
2873
- return null;
2874
2853
  };
2875
2854
  }
2855
+ function createGitHubTools(getAccessToken, requestGitHubAccess) {
2856
+ return [
2857
+ createGetAuthenticatedUserTool(getAccessToken, requestGitHubAccess),
2858
+ createGitHubApiTool(getAccessToken, requestGitHubAccess)
2859
+ ];
2860
+ }
2876
2861
 
2877
2862
  // src/tools/gmail.ts
2878
2863
  var GMAIL_PROVIDER = "gmail";
@@ -3735,7 +3720,7 @@ async function callSlack(callProxy, path, query, requestBody) {
3735
3720
  const { status, json } = await callProxy(path, query, requestBody);
3736
3721
  return interpretSlackResult(path, status, json);
3737
3722
  }
3738
- function readString(raw) {
3723
+ function readString2(raw) {
3739
3724
  return typeof raw === "string" ? raw : "";
3740
3725
  }
3741
3726
  function clampLimit(raw, fallback, min, max) {
@@ -4297,7 +4282,7 @@ function createSlackSearchMessagesTool(callProxy) {
4297
4282
  }
4298
4283
  },
4299
4284
  executor: async (args) => searchSlackMessages(callProxy, {
4300
- query: readString(args.query),
4285
+ query: readString2(args.query),
4301
4286
  count: args.count,
4302
4287
  channel: typeof args.channel === "string" ? args.channel : void 0,
4303
4288
  from_user: typeof args.from_user === "string" ? args.from_user : void 0,
@@ -4374,7 +4359,7 @@ function createSlackGetChannelHistoryTool(callProxy) {
4374
4359
  }
4375
4360
  },
4376
4361
  executor: async (args) => getSlackChannelHistory(callProxy, {
4377
- channel: readString(args.channel),
4362
+ channel: readString2(args.channel),
4378
4363
  limit: args.limit
4379
4364
  })
4380
4365
  };
@@ -4405,8 +4390,8 @@ function createSlackGetThreadRepliesTool(callProxy) {
4405
4390
  }
4406
4391
  },
4407
4392
  executor: async (args) => getSlackThreadReplies(callProxy, {
4408
- channel: readString(args.channel),
4409
- ts: readString(args.ts),
4393
+ channel: readString2(args.channel),
4394
+ ts: readString2(args.ts),
4410
4395
  limit: args.limit
4411
4396
  })
4412
4397
  };
@@ -4437,8 +4422,8 @@ function createSlackPostMessageTool(callProxy) {
4437
4422
  }
4438
4423
  },
4439
4424
  executor: async (args) => postSlackMessage(callProxy, {
4440
- channel: readString(args.channel),
4441
- text: readString(args.text),
4425
+ channel: readString2(args.channel),
4426
+ text: readString2(args.text),
4442
4427
  thread_ts: typeof args.thread_ts === "string" ? args.thread_ts : void 0
4443
4428
  })
4444
4429
  };
@@ -14050,6 +14035,7 @@ export {
14050
14035
  createNotionGetTeamsTool,
14051
14036
  createNotionGetUsersTool,
14052
14037
  createNotionMovePagesTool,
14038
+ createNotionProxyTools,
14053
14039
  createNotionSearchTool,
14054
14040
  createNotionTools,
14055
14041
  createNotionUpdateDataSourceTool,