@ziggs-ai/agent-sdk 0.1.12 → 0.2.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.
Files changed (218) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +41 -2
  3. package/dist/AgentHost.d.ts +201 -0
  4. package/dist/AgentHost.js +652 -0
  5. package/dist/adapters/AnthropicAdapter.d.ts +39 -0
  6. package/dist/adapters/AnthropicAdapter.js +200 -0
  7. package/dist/adapters/McpAdapter.d.ts +199 -0
  8. package/dist/adapters/McpAdapter.js +468 -0
  9. package/dist/adapters/OpenAIAdapter.d.ts +26 -0
  10. package/dist/adapters/OpenAIAdapter.js +158 -0
  11. package/dist/adapters/index.js +2 -0
  12. package/dist/agent/Agent.d.ts +78 -0
  13. package/dist/agent/Agent.js +58 -0
  14. package/dist/agent/ClaudeSdkAgent.d.ts +38 -0
  15. package/dist/agent/ClaudeSdkAgent.js +322 -0
  16. package/dist/agent/claudeCognition.d.ts +67 -0
  17. package/dist/agent/claudeCognition.js +103 -0
  18. package/dist/agent/claudeSdkProtocol.d.ts +11 -0
  19. package/dist/agent/claudeSdkTools.d.ts +19 -0
  20. package/dist/agent/claudeSdkTools.js +57 -0
  21. package/dist/agent/hostedAgentConnection.d.ts +15 -0
  22. package/dist/agent/hostedAgentConnection.js +29 -0
  23. package/dist/agent/toolManagerToSdkMcp.d.ts +12 -0
  24. package/dist/agent/toolManagerToSdkMcp.js +123 -0
  25. package/dist/brain/index.d.ts +50 -0
  26. package/dist/brain/index.js +45 -0
  27. package/dist/cognition/validateContext.d.ts +11 -0
  28. package/dist/cognition/validateContext.js +117 -0
  29. package/dist/context/applyEffects.d.ts +2 -0
  30. package/dist/context/applyEffects.js +89 -0
  31. package/dist/context/batch.d.ts +2 -0
  32. package/dist/context/batch.js +18 -0
  33. package/dist/context/classifyEnvelope.d.ts +1 -0
  34. package/dist/context/classifyEnvelope.js +58 -0
  35. package/dist/context/routingLabels.d.ts +4 -0
  36. package/dist/context/routingLabels.js +48 -0
  37. package/dist/defineAgent.d.ts +14 -0
  38. package/dist/defineAgent.js +68 -0
  39. package/dist/formatters/AgreementFormatter.d.ts +18 -0
  40. package/dist/formatters/AgreementFormatter.js +130 -0
  41. package/dist/formatters/HistoryFormatter.d.ts +26 -0
  42. package/dist/formatters/HistoryFormatter.js +173 -0
  43. package/dist/formatters/index.js +2 -0
  44. package/{src/index.ts → dist/index.d.ts} +7 -53
  45. package/dist/index.js +66 -0
  46. package/dist/ingress/normalizeIncoming.d.ts +65 -0
  47. package/dist/ingress/normalizeIncoming.js +88 -0
  48. package/dist/memory/MemoryStore.d.ts +55 -0
  49. package/dist/memory/MemoryStore.js +78 -0
  50. package/dist/pricing/fleetDefaults.d.ts +46 -0
  51. package/dist/pricing/fleetDefaults.js +168 -0
  52. package/dist/pricing/fleetEvalFree.d.ts +7 -0
  53. package/dist/pricing/fleetEvalFree.js +24 -0
  54. package/dist/pricing/fleetFreeTierA.gen.d.ts +6 -0
  55. package/{src/pricing/fleetFreeTierA.gen.ts → dist/pricing/fleetFreeTierA.gen.js} +5 -6
  56. package/dist/pricing/fleetTierByAgentId.gen.d.ts +7 -0
  57. package/dist/pricing/fleetTierByAgentId.gen.js +768 -0
  58. package/dist/runtime/AgentMachine.d.ts +143 -0
  59. package/dist/runtime/AgentMachine.js +400 -0
  60. package/dist/runtime/PromptBuilder.d.ts +96 -0
  61. package/dist/runtime/PromptBuilder.js +439 -0
  62. package/{src/runtime/bootstrapCtx.ts → dist/runtime/bootstrapCtx.d.ts} +1 -22
  63. package/dist/runtime/bootstrapCtx.js +37 -0
  64. package/dist/runtime/buildOutcome.d.ts +68 -0
  65. package/dist/runtime/buildOutcome.js +464 -0
  66. package/dist/runtime/defaults.d.ts +54 -0
  67. package/dist/runtime/defaults.js +47 -0
  68. package/dist/runtime/deriveRespondsToProposals.d.ts +9 -0
  69. package/dist/runtime/deriveRespondsToProposals.js +38 -0
  70. package/dist/runtime/kickoff.d.ts +15 -0
  71. package/dist/runtime/kickoff.js +112 -0
  72. package/dist/runtime/runMechanical.d.ts +25 -0
  73. package/dist/runtime/runMechanical.js +74 -0
  74. package/dist/runtime/runTurn.d.ts +63 -0
  75. package/dist/runtime/runTurn.js +800 -0
  76. package/dist/runtime/validateWorkflow.d.ts +17 -0
  77. package/dist/runtime/validateWorkflow.js +209 -0
  78. package/dist/server/ConnectionPool.d.ts +68 -0
  79. package/dist/server/ConnectionPool.js +166 -0
  80. package/dist/server/EventQueue.d.ts +33 -0
  81. package/dist/server/EventQueue.js +98 -0
  82. package/dist/server/InboxCatchUp.d.ts +24 -0
  83. package/dist/server/InboxCatchUp.js +214 -0
  84. package/dist/server/OutboxBuffer.d.ts +33 -0
  85. package/dist/server/OutboxBuffer.js +68 -0
  86. package/dist/server/SeenMessages.d.ts +13 -0
  87. package/dist/server/SeenMessages.js +31 -0
  88. package/dist/server/StateTimers.d.ts +34 -0
  89. package/dist/server/StateTimers.js +70 -0
  90. package/dist/server/ZiggsEffectHandler.d.ts +79 -0
  91. package/dist/server/ZiggsEffectHandler.js +319 -0
  92. package/dist/server/agreements/AgreementService.d.ts +50 -0
  93. package/dist/server/agreements/AgreementService.js +85 -0
  94. package/dist/server/createHealthServer.d.ts +7 -0
  95. package/dist/server/createHealthServer.js +75 -0
  96. package/dist/server/proactive/ProactiveTrigger.d.ts +33 -0
  97. package/dist/server/proactive/ProactiveTrigger.js +71 -0
  98. package/dist/server/runLauncher.d.ts +28 -0
  99. package/dist/server/runLauncher.js +93 -0
  100. package/dist/server/tasks/TaskService.d.ts +25 -0
  101. package/dist/server/tasks/TaskService.js +103 -0
  102. package/dist/server/tasks/index.js +1 -0
  103. package/dist/server/telemetryIngest.d.ts +24 -0
  104. package/dist/server/telemetryIngest.js +62 -0
  105. package/dist/server/tools/index.d.ts +24 -0
  106. package/{src/server/tools/index.ts → dist/server/tools/index.js} +4 -24
  107. package/dist/server/tools/tier1/protocolRunner.d.ts +39 -0
  108. package/dist/server/tools/tier1/protocolRunner.js +288 -0
  109. package/dist/server/tools/tier1/protocolTools.d.ts +12 -0
  110. package/dist/server/tools/tier1/protocolTools.js +37 -0
  111. package/dist/server/tools/tier2/connectionTools.d.ts +4 -0
  112. package/dist/server/tools/tier2/connectionTools.js +63 -0
  113. package/dist/server/tools/tier2/contextTools.d.ts +4 -0
  114. package/dist/server/tools/tier2/contextTools.js +46 -0
  115. package/dist/server/tools/tier2/discoveryTools.d.ts +4 -0
  116. package/dist/server/tools/tier2/discoveryTools.js +25 -0
  117. package/dist/server/tools/tier2/marketplaceTools.d.ts +3 -0
  118. package/dist/server/tools/tier2/marketplaceTools.js +22 -0
  119. package/dist/server/tools/tier2/paymentTools.d.ts +12 -0
  120. package/dist/server/tools/tier2/paymentTools.js +165 -0
  121. package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +35 -0
  122. package/dist/server/ziggsconnect/ZiggsConnectClient.js +95 -0
  123. package/dist/server/ziggscontext/ZiggsContextClient.d.ts +53 -0
  124. package/dist/server/ziggscontext/ZiggsContextClient.js +124 -0
  125. package/dist/server/ziggspay/ZiggsPayClient.d.ts +82 -0
  126. package/dist/server/ziggspay/ZiggsPayClient.js +203 -0
  127. package/dist/shared/ids.d.ts +1 -0
  128. package/dist/shared/ids.js +3 -0
  129. package/dist/shared/runtimeLog.d.ts +18 -0
  130. package/dist/shared/runtimeLog.js +68 -0
  131. package/dist/shared/types.d.ts +22 -0
  132. package/dist/shared/types.js +23 -0
  133. package/dist/tasks/protocolRegistry.d.ts +16 -0
  134. package/dist/tasks/protocolRegistry.js +20 -0
  135. package/dist/tools/ToolManager.d.ts +12 -0
  136. package/dist/tools/ToolManager.js +74 -0
  137. package/dist/tools/ToolProvider.d.ts +6 -0
  138. package/dist/tools/ToolProvider.js +21 -0
  139. package/dist/tools/defineTool.d.ts +27 -0
  140. package/dist/tools/defineTool.js +62 -0
  141. package/{src/tools/index.ts → dist/tools/index.d.ts} +0 -1
  142. package/dist/tools/index.js +6 -0
  143. package/dist/tools/recordReport.d.ts +41 -0
  144. package/dist/tools/recordReport.js +30 -0
  145. package/dist/types.d.ts +466 -0
  146. package/dist/types.js +7 -0
  147. package/dist/utils/jsonExtractor.d.ts +2 -0
  148. package/dist/utils/jsonExtractor.js +111 -0
  149. package/package.json +14 -6
  150. package/src/AgentHost.ts +0 -814
  151. package/src/adapters/AnthropicAdapter.ts +0 -238
  152. package/src/adapters/McpAdapter.ts +0 -678
  153. package/src/adapters/OpenAIAdapter.ts +0 -194
  154. package/src/agent/Agent.ts +0 -112
  155. package/src/agent/ClaudeSdkAgent.ts +0 -401
  156. package/src/agent/claudeCognition.ts +0 -139
  157. package/src/agent/claudeSdkTools.ts +0 -81
  158. package/src/agent/hostedAgentConnection.ts +0 -41
  159. package/src/agent/toolManagerToSdkMcp.ts +0 -136
  160. package/src/cognition/validateContext.ts +0 -95
  161. package/src/context/applyEffects.ts +0 -80
  162. package/src/context/batch.ts +0 -19
  163. package/src/context/classifyEnvelope.ts +0 -38
  164. package/src/context/routingLabels.ts +0 -46
  165. package/src/defineAgent.ts +0 -77
  166. package/src/formatters/AgreementFormatter.ts +0 -111
  167. package/src/formatters/HistoryFormatter.ts +0 -172
  168. package/src/ingress/normalizeIncoming.ts +0 -162
  169. package/src/memory/MemoryStore.ts +0 -104
  170. package/src/pricing/fleetDefaults.ts +0 -225
  171. package/src/pricing/fleetEvalFree.ts +0 -24
  172. package/src/pricing/fleetTierByAgentId.gen.ts +0 -775
  173. package/src/runtime/AgentMachine.ts +0 -484
  174. package/src/runtime/PromptBuilder.ts +0 -522
  175. package/src/runtime/buildOutcome.ts +0 -563
  176. package/src/runtime/defaults.ts +0 -86
  177. package/src/runtime/deriveRespondsToProposals.ts +0 -35
  178. package/src/runtime/kickoff.ts +0 -117
  179. package/src/runtime/runMechanical.ts +0 -106
  180. package/src/runtime/runTurn.ts +0 -870
  181. package/src/runtime/validateWorkflow.ts +0 -221
  182. package/src/server/ConnectionPool.ts +0 -206
  183. package/src/server/EventQueue.ts +0 -133
  184. package/src/server/InboxCatchUp.ts +0 -291
  185. package/src/server/OutboxBuffer.ts +0 -90
  186. package/src/server/SeenMessages.ts +0 -27
  187. package/src/server/StateTimers.ts +0 -85
  188. package/src/server/ZiggsEffectHandler.ts +0 -427
  189. package/src/server/agreements/AgreementService.ts +0 -118
  190. package/src/server/createHealthServer.ts +0 -85
  191. package/src/server/proactive/ProactiveTrigger.ts +0 -83
  192. package/src/server/runLauncher.ts +0 -146
  193. package/src/server/tasks/TaskService.ts +0 -111
  194. package/src/server/telemetryIngest.ts +0 -91
  195. package/src/server/tools/tier1/protocolRunner.ts +0 -353
  196. package/src/server/tools/tier1/protocolTools.ts +0 -114
  197. package/src/server/tools/tier2/connectionTools.ts +0 -75
  198. package/src/server/tools/tier2/contextTools.ts +0 -63
  199. package/src/server/tools/tier2/discoveryTools.ts +0 -34
  200. package/src/server/tools/tier2/marketplaceTools.ts +0 -27
  201. package/src/server/tools/tier2/paymentTools.ts +0 -170
  202. package/src/server/ziggsconnect/ZiggsConnectClient.ts +0 -126
  203. package/src/server/ziggscontext/ZiggsContextClient.ts +0 -129
  204. package/src/server/ziggspay/ZiggsPayClient.ts +0 -193
  205. package/src/shared/ids.ts +0 -3
  206. package/src/shared/runtimeLog.ts +0 -72
  207. package/src/shared/types.ts +0 -29
  208. package/src/tasks/protocolRegistry.ts +0 -25
  209. package/src/tools/ToolManager.ts +0 -95
  210. package/src/tools/ToolProvider.ts +0 -19
  211. package/src/tools/defineTool.ts +0 -90
  212. package/src/tools/recordReport.ts +0 -82
  213. package/src/types.ts +0 -483
  214. package/src/utils/jsonExtractor.ts +0 -100
  215. /package/{src/adapters/index.ts → dist/adapters/index.d.ts} +0 -0
  216. /package/{src/agent/claudeSdkProtocol.ts → dist/agent/claudeSdkProtocol.js} +0 -0
  217. /package/{src/formatters/index.ts → dist/formatters/index.d.ts} +0 -0
  218. /package/{src/server/tasks/index.ts → dist/server/tasks/index.d.ts} +0 -0
@@ -1,678 +0,0 @@
1
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
- import { AgentHost, type AgentHostOptions } from '../AgentHost.js';
5
- import type { ToolDefinition } from '../tools/defineTool.js';
6
- import { thinkingDefaults } from '../runtime/defaults.js';
7
- import type { Workflow } from '../types.js';
8
-
9
- export type McpTransportConfig =
10
- | { type: 'stdio'; command: string; args?: string[]; env?: Record<string, string> }
11
- | {
12
- type: 'http';
13
- url: string;
14
- /**
15
- * Optional static request headers — e.g. a fixed API key the server is
16
- * keyed by. Sent on every request to the server. Values are used as-is;
17
- * `$VAR` interpolation (if any) is the caller's job, mirroring how the
18
- * stdio path leaves `env` interpolation to the caller.
19
- */
20
- headers?: Record<string, string>;
21
- }
22
- | {
23
- /**
24
- * Brokered connection through the Ziggs MCP gateway: the agent speaks
25
- * StreamableHTTP to `/connections/:id/mcp` and Ziggs injects the
26
- * connection owner's OAuth token upstream. The agent never holds the
27
- * token; every tools/call is checked against the grant's caveats.
28
- */
29
- type: 'ziggs';
30
- /** Connection id (`con_…`) of the user's MCP server connection. */
31
- connectionId: string;
32
- /** ConnectionGrant id (`cgr_…`) issued to this agent. */
33
- grantId: string;
34
- /** Operator key — defaults to ZIGGS_OPERATOR_KEY. */
35
- operatorKey?: string;
36
- /** Ziggs backend base URL — defaults to BACKEND_URL / ZIGGS_BACKEND_URL. */
37
- baseUrl?: string;
38
- /** Agent to impersonate (X-Agent-Id) — defaults to the host agentId. */
39
- agentId?: string;
40
- }
41
- | {
42
- /**
43
- * Per-user brokered connection (ZIG-771): the agent holds NO
44
- * connectionId/grantId. It speaks StreamableHTTP to
45
- * `/connections/mcp/relay` and asserts the invoking end-user via
46
- * `x-on-behalf-of-user`; the gateway resolves that user's own connection
47
- * + grant and injects their token. One MCP client is created per invoking
48
- * user so each keeps an isolated upstream session. Tool schemas cannot be
49
- * discovered at boot (there is no connection yet) — pass `toolSchema`.
50
- */
51
- type: 'ziggs-per-user';
52
- /** Operator key — defaults to ZIGGS_OPERATOR_KEY. */
53
- operatorKey?: string;
54
- /** Ziggs backend base URL — defaults to BACKEND_URL / ZIGGS_BACKEND_URL. */
55
- baseUrl?: string;
56
- /** Agent to impersonate (X-Agent-Id) — defaults to the host agentId. */
57
- agentId?: string;
58
- /**
59
- * The MCP server URL this agent fronts. Not used for gateway calls (the
60
- * gateway derives the server from the resolved connection), but needed to
61
- * open a connection request when the invoking user hasn't connected yet
62
- * (ZIG-771). Without it, a NO_USER_CONNECTION surfaces as an error rather
63
- * than an auto-created consent prompt.
64
- */
65
- serverUrl?: string;
66
- };
67
-
68
- /**
69
- * Typed failure from the Ziggs MCP gateway (ZIG-702). `code` is the stable
70
- * machine-readable reason — e.g. `GRANT_REVOKED`, `GRANT_EXPIRED`,
71
- * `GRANT_HOLDER_MISMATCH`, `GRANT_CONNECTION_MISMATCH` — so agent code can
72
- * branch ("revoked → tell the user and stop retrying" vs "network blip →
73
- * retry") instead of parsing transport-error prose.
74
- */
75
- export class ZiggsGatewayError extends Error {
76
- readonly code: string;
77
- readonly status: number;
78
-
79
- constructor(code: string, message: string, status: number) {
80
- super(message);
81
- this.name = 'ZiggsGatewayError';
82
- this.code = code;
83
- this.status = status;
84
- }
85
- }
86
-
87
- /**
88
- * Wrap fetch so structured gateway denials (401/403 with a `code` body)
89
- * surface as ZiggsGatewayError instead of a generic transport failure.
90
- */
91
- export function wrapGatewayFetch(
92
- baseFetch: typeof fetch = fetch,
93
- ): typeof fetch {
94
- return (async (input: RequestInfo | URL, init?: RequestInit) => {
95
- const response = await baseFetch(input, init);
96
- if (response.status === 401 || response.status === 403) {
97
- const body = (await response
98
- .clone()
99
- .json()
100
- .catch(() => null)) as { code?: unknown; message?: unknown } | null;
101
- if (body && typeof body.code === 'string') {
102
- throw new ZiggsGatewayError(
103
- body.code,
104
- typeof body.message === 'string' ? body.message : body.code,
105
- response.status,
106
- );
107
- }
108
- }
109
- return response;
110
- }) as typeof fetch;
111
- }
112
-
113
- /** Resolve the gateway URL + auth headers for a `type: 'ziggs'` transport. */
114
- export function resolveZiggsGatewayTarget(
115
- config: Extract<McpTransportConfig, { type: 'ziggs' }>,
116
- defaults: { agentId?: string } = {},
117
- ): { url: URL; headers: Record<string, string> } {
118
- if (!config.connectionId) {
119
- throw new Error('ziggs MCP transport requires a connectionId');
120
- }
121
- if (!config.grantId) {
122
- throw new Error('ziggs MCP transport requires a grantId');
123
- }
124
- const operatorKey = config.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
125
- if (!operatorKey) {
126
- throw new Error(
127
- 'ziggs MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)',
128
- );
129
- }
130
- const agentId = config.agentId || defaults.agentId;
131
- if (!agentId) {
132
- throw new Error(
133
- 'ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)',
134
- );
135
- }
136
- const baseUrl = (
137
- config.baseUrl ||
138
- process.env.BACKEND_URL ||
139
- process.env.ZIGGS_BACKEND_URL ||
140
- 'http://localhost:3000'
141
- ).replace(/\/$/, '');
142
- return {
143
- url: new URL(`${baseUrl}/connections/${encodeURIComponent(config.connectionId)}/mcp`),
144
- headers: {
145
- authorization: `Bearer ${operatorKey}`,
146
- 'x-agent-id': agentId,
147
- 'x-grant-id': config.grantId,
148
- },
149
- };
150
- }
151
-
152
- /**
153
- * Resolve the per-user gateway URL + headers (ZIG-771). No connectionId/grantId —
154
- * the gateway resolves the invoking user's own connection + grant from
155
- * `x-on-behalf-of-user` + the impersonated agent.
156
- */
157
- export function resolvePerUserGatewayTarget(
158
- config: Extract<McpTransportConfig, { type: 'ziggs-per-user' }>,
159
- agentId: string,
160
- userId: string,
161
- onBehalfOfAgreementId?: string,
162
- ): { url: URL; headers: Record<string, string> } {
163
- const operatorKey = config.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
164
- if (!operatorKey) {
165
- throw new Error(
166
- 'ziggs-per-user MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)',
167
- );
168
- }
169
- const baseUrl = (
170
- config.baseUrl ||
171
- process.env.BACKEND_URL ||
172
- process.env.ZIGGS_BACKEND_URL ||
173
- 'http://localhost:3000'
174
- ).replace(/\/$/, '');
175
- return {
176
- url: new URL(`${baseUrl}/connections/mcp/relay`),
177
- headers: {
178
- authorization: `Bearer ${operatorKey}`,
179
- 'x-agent-id': agentId,
180
- 'x-on-behalf-of-user': userId,
181
- // ZIG-831: when this call runs under a delegation, userId is the delegating
182
- // agent (senderId), not the end-user who owns the connection. The gateway
183
- // uses this agreement to resolve the delegation root's user.
184
- ...(onBehalfOfAgreementId ? { 'x-on-behalf-of-agreement': onBehalfOfAgreementId } : {}),
185
- },
186
- };
187
- }
188
-
189
- export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' | 'workflow' | 'states' | 'initial'> {
190
- /** MCP server connection — stdio (local process) or HTTP (remote). */
191
- transport: McpTransportConfig;
192
- /** Additional Ziggs tools to register alongside the MCP-derived ones. */
193
- extraTools?: ToolDefinition[];
194
- /** Override the default single-state thinking workflow. */
195
- workflow?: Workflow;
196
- /**
197
- * Max tools to bind to the LLM per turn. LLM providers cap the `tools` array
198
- * (OpenAI: 128); an MCP server exposing more (e.g. PostHog: 146) makes every
199
- * turn fail with `400 ... array too long`. When the server's tool count
200
- * exceeds this budget the list is truncated (and logged). Defaults to 128.
201
- * See ZIG-759; Phase 2 will replace truncation with query-aware selection.
202
- */
203
- maxTools?: number;
204
- /**
205
- * Restrict the bound tool set to only tools whose name matches one of these
206
- * case-insensitive substrings, applied BEFORE the maxTools cap. Lets a large
207
- * MCP server (e.g. PostHog's 146 tools) be narrowed to the handful this agent
208
- * actually needs — cutting per-turn cost and stopping the model from thrashing
209
- * across (or never binding) the tool it should call. Empty/undefined = bind
210
- * all (unchanged behavior). ZIG-759 Phase 2 = name-based selection.
211
- */
212
- allowedTools?: string[];
213
- /**
214
- * Pre-discovered MCP tool schemas. REQUIRED for `type: 'ziggs-per-user'`
215
- * (ZIG-771) — there is no boot connection to call `tools/list` on, so the
216
- * schema is baked at provision time and passed here. Ignored for the other
217
- * transports, which discover tools live at boot.
218
- */
219
- toolSchema?: Array<{
220
- name: string;
221
- description?: string;
222
- inputSchema?: Record<string, unknown>;
223
- }>;
224
- }
225
-
226
- /** Provider `tools` array cap (OpenAI's is 128). */
227
- const DEFAULT_MAX_TOOLS = 128;
228
- /** Headroom reserved for control actions (wait/respond) + future additions. */
229
- const CONTROL_TOOL_RESERVE = 4;
230
-
231
- /**
232
- * How many MCP tools may be bound to the LLM, given the provider cap and the
233
- * number of extra (non-MCP) tools the agent also carries. Always >= 1 so a
234
- * pathological config still binds something rather than nothing. ZIG-759.
235
- */
236
- export function mcpToolBudget(maxTools: number, extraToolCount: number): number {
237
- return Math.max(1, maxTools - CONTROL_TOOL_RESERVE - extraToolCount);
238
- }
239
-
240
- export interface McpAgentHandle {
241
- host: AgentHost;
242
- /** Disconnect the MCP client (call on shutdown alongside host.disconnect()). */
243
- closeMcp: () => Promise<void>;
244
- /** Names of MCP tools that were mapped to Ziggs tools on startup. */
245
- toolNames: string[];
246
- }
247
-
248
- export function buildTransport(config: McpTransportConfig, defaults: { agentId?: string } = {}) {
249
- if (config.type === 'stdio') {
250
- return new StdioClientTransport({
251
- command: config.command,
252
- args: config.args ?? [],
253
- env: config.env,
254
- });
255
- }
256
- if (config.type === 'ziggs') {
257
- const target = resolveZiggsGatewayTarget(config, defaults);
258
- return new StreamableHTTPClientTransport(target.url, {
259
- requestInit: { headers: target.headers },
260
- fetch: wrapGatewayFetch(),
261
- });
262
- }
263
- if (config.type === 'ziggs-per-user') {
264
- // Per-user transports are built lazily per invoking user in createMcpAgent
265
- // (each needs a different x-on-behalf-of-user header), not here.
266
- throw new Error('ziggs-per-user transport is created per invoking user, not via buildTransport');
267
- }
268
- return new StreamableHTTPClientTransport(
269
- new URL(config.url),
270
- config.headers && Object.keys(config.headers).length > 0
271
- ? { requestInit: { headers: config.headers } }
272
- : undefined,
273
- );
274
- }
275
-
276
- type McpTool = {
277
- name: string;
278
- description?: string;
279
- inputSchema?: Record<string, unknown>;
280
- };
281
-
282
- type McpContent = { type: string; text?: string };
283
-
284
- function mcpContentToResult(content: McpContent[]): unknown {
285
- const text = content
286
- .map(c => (c.type === 'text' ? c.text ?? '' : JSON.stringify(c)))
287
- .join('\n');
288
- try {
289
- return JSON.parse(text);
290
- } catch {
291
- return text;
292
- }
293
- }
294
-
295
- /**
296
- * Calls one MCP tool. The single-client path closes over a fixed client; the
297
- * per-user path (ZIG-771) uses `context.senderId` to pick that user's client.
298
- */
299
- type McpToolCaller = (
300
- toolName: string,
301
- args: Record<string, unknown>,
302
- context: Record<string, unknown>,
303
- ) => Promise<{ isError?: boolean; content: McpContent[] }>;
304
-
305
- function mapMcpTools(callTool: McpToolCaller, tools: McpTool[]): ToolDefinition[] {
306
- return tools.map(tool => ({
307
- schema: {
308
- type: 'function' as const,
309
- function: {
310
- name: tool.name,
311
- description: tool.description ?? '',
312
- parameters: tool.inputSchema ?? { type: 'object', properties: {} },
313
- },
314
- },
315
- handler: async (
316
- args: Record<string, unknown>,
317
- context: Record<string, unknown>,
318
- ) => {
319
- const result = await callTool(tool.name, args, context);
320
- if (result.isError) {
321
- throw new Error(mcpContentToResult(result.content) as string);
322
- }
323
- return mcpContentToResult(result.content);
324
- },
325
- isGenericFallback: true,
326
- }));
327
- }
328
-
329
- function buildDefaultWorkflow(description: string, mcpTools: McpTool[]): Workflow {
330
- const dflt = thinkingDefaults({ initial: 'idle' });
331
- // Bind each MCP tool as an explicit action so runTurn includes them in the
332
- // LLM API call and the decision schema. Without this, allowedToolNames is
333
- // empty and the tools array passed to the LLM is also empty.
334
- const toolActions = Object.fromEntries(
335
- mcpTools.map(t => [t.name, {
336
- tool: t.name,
337
- prompt: { when: t.description || `Use ${t.name}` },
338
- }]),
339
- );
340
- return {
341
- id: 'mcp-agent',
342
- description: description || 'MCP-backed agent',
343
- initial: 'idle',
344
- states: {
345
- idle: {
346
- kind: 'thinking',
347
- prompt: {
348
- role: 'You are a helpful agent powered by an MCP server.',
349
- goal: description || 'Help the user by calling the available tools.',
350
- constraints: 'Use your tools to answer the user\'s request. After gathering results, use "respond" to send the answer. Use "wait" only when no action is needed.',
351
- },
352
- actions: {
353
- ...dflt.actions,
354
- ...toolActions,
355
- respond: {
356
- prompt: {
357
- instruction: 'Send your final answer to the user.',
358
- when: 'You have gathered enough information and are ready to reply to the user.',
359
- format: 'args: { receiverId: "<userId>", message: "<your answer>" }',
360
- },
361
- produces: 'message-sent',
362
- },
363
- },
364
- transitions: [
365
- { to: 'idle', when: (o) => o.kind === 'wait' },
366
- { to: 'idle', when: (o) => o.kind === 'error' },
367
- { to: 'done', when: (o) => o.kind === 'message-sent' },
368
- ],
369
- },
370
- done: {
371
- kind: 'parked',
372
- transitions: [
373
- { to: 'idle', when: (o) => o.kind === 'message-received' },
374
- ],
375
- },
376
- },
377
- };
378
- }
379
-
380
- /**
381
- * Max chars for a single tool description bound to the LLM. Verbose MCP servers
382
- * embed whole manuals in their descriptions (PostHog's `execute-sql` alone is
383
- * ~30K chars of HogQL docs; its 146 tools total ~54K tokens of descriptions) and
384
- * that full block rides every turn. Keep the lead — what the tool does + how to
385
- * call it — and drop the embedded reference; agents fetch detail via dedicated
386
- * schema/help tools at call time. Tunable; big enough to preserve real guidance.
387
- * ZIG-759.
388
- */
389
- export const MAX_TOOL_DESC_CHARS = 2000;
390
-
391
- /** Truncate one description at a sentence/newline boundary near the cap. */
392
- export function trimDescription(desc: string | undefined): string | undefined {
393
- if (!desc || desc.length <= MAX_TOOL_DESC_CHARS) return desc;
394
- const slice = desc.slice(0, MAX_TOOL_DESC_CHARS);
395
- const boundary = Math.max(slice.lastIndexOf('\n'), slice.lastIndexOf('. '));
396
- const head = boundary > MAX_TOOL_DESC_CHARS * 0.6 ? slice.slice(0, boundary + 1) : slice;
397
- return `${head.trimEnd()} […description truncated; use the server's schema/help tools for full detail]`;
398
- }
399
-
400
- /** Trim oversized descriptions across the tool set (cost control, ZIG-759). */
401
- function trimToolDescriptions(tools: McpTool[], agentId: string | undefined): McpTool[] {
402
- let trimmedCount = 0;
403
- let savedChars = 0;
404
- const out = tools.map((t) => {
405
- const nextDesc = trimDescription(t.description);
406
- if (nextDesc === t.description) return t;
407
- trimmedCount += 1;
408
- savedChars += (t.description?.length ?? 0) - (nextDesc?.length ?? 0);
409
- return { ...t, description: nextDesc };
410
- });
411
- if (trimmedCount > 0) {
412
- console.warn(
413
- `[McpAdapter] ${agentId}: trimmed ${trimmedCount} oversized tool description(s) to ${MAX_TOOL_DESC_CHARS} chars, saving ~${Math.round(savedChars / 4 / 1000)}K tokens/turn. (ZIG-759)`,
414
- );
415
- }
416
- return out;
417
- }
418
-
419
- /**
420
- * Keep only tools whose name matches one of the allowlist substrings
421
- * (case-insensitive). No allowlist → pass through unchanged. Applied before the
422
- * maxTools cap so a targeted agent binds exactly the tools it needs. ZIG-759.
423
- */
424
- function filterToolsByAllowlist(
425
- tools: McpTool[],
426
- allowedTools: string[] | undefined,
427
- agentId: string | undefined,
428
- ): McpTool[] {
429
- if (!allowedTools || allowedTools.length === 0) return tools;
430
- const patterns = allowedTools.map((p) => p.toLowerCase());
431
- const kept = tools.filter((t) =>
432
- patterns.some((p) => t.name.toLowerCase().includes(p)),
433
- );
434
- console.warn(
435
- `[McpAdapter] ${agentId}: tool allowlist active — bound ${kept.length}/${tools.length} tools matching [${allowedTools.join(', ')}]${kept.length === 0 ? ' ⚠️ NONE MATCHED — check patterns against the server\'s tool names' : ': ' + kept.map((t) => t.name).join(', ')}.`,
436
- );
437
- return kept;
438
- }
439
-
440
- /**
441
- * Cap the tool set bound to the LLM at the provider limit (OpenAI 128) —
442
- * truncate and log rather than crash — and trim oversized descriptions so the
443
- * bound schema doesn't ship a manual every turn. ZIG-759 Phase 1. Shared by the
444
- * boot-time (single-client) and per-user paths. When `allowedTools` is set the
445
- * server's tools are narrowed by name first, so the cap rarely bites.
446
- */
447
- function capTools(
448
- tools: McpTool[],
449
- agentId: string | undefined,
450
- maxTools: number,
451
- extraToolCount: number,
452
- allowedTools?: string[],
453
- ): McpTool[] {
454
- const allowed = filterToolsByAllowlist(tools, allowedTools, agentId);
455
- const trimmed = trimToolDescriptions(allowed, agentId);
456
- const budget = mcpToolBudget(maxTools, extraToolCount);
457
- if (trimmed.length <= budget) return trimmed;
458
- console.warn(
459
- `[McpAdapter] ${agentId}: MCP server exposes ${trimmed.length} tools, over the ${maxTools}-tool LLM cap; binding the first ${budget}, dropping ${trimmed.length - budget}. (ZIG-759 Phase 2 will select per query.)`,
460
- );
461
- return trimmed.slice(0, budget);
462
- }
463
-
464
- /**
465
- * Wraps any MCP server as a Ziggs AgentHost.
466
- *
467
- * 1. Opens a connection to the MCP server (stdio, HTTP, or the Ziggs gateway).
468
- * 2. Calls tools/list and maps each tool to a Ziggs ToolDefinition.
469
- * 3. Returns an AgentHost pre-loaded with those tools, ready to connectAsync().
470
- *
471
- * Usage (local stdio server):
472
- * const { host, closeMcp } = await createMcpAgent({
473
- * transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
474
- * agentId: 'my-github-agent',
475
- * name: 'GitHub Agent',
476
- * description: 'Wraps the GitHub MCP server',
477
- * });
478
- * await host.connectAsync();
479
- *
480
- * Usage (OAuth-protected remote server, brokered through Ziggs — the user
481
- * connected the server and issued this agent a grant; the agent never sees
482
- * the user's token):
483
- * const { host, closeMcp } = await createMcpAgent({
484
- * transport: { type: 'ziggs', connectionId: 'con_abc', grantId: 'cgr_xyz' },
485
- * agentId: 'my-notion-agent',
486
- * name: 'Notion Agent',
487
- * description: 'Works in the owner\'s Notion via their MCP connection',
488
- * });
489
- * await host.connectAsync();
490
- *
491
- * process.on('SIGTERM', async () => {
492
- * host.disconnect();
493
- * await host.drain();
494
- * await closeMcp();
495
- * });
496
- */
497
- export async function createMcpAgent(options: CreateMcpAgentOptions): Promise<McpAgentHandle> {
498
- const { transport: transportConfig, extraTools = [], workflow, maxTools = DEFAULT_MAX_TOOLS, allowedTools, toolSchema, ...hostOptions } = options;
499
-
500
- // ── Per-user path (ZIG-771): no boot connection; one MCP client per invoking
501
- // user, each carrying that user's on-behalf-of header for an isolated session.
502
- if (transportConfig.type === 'ziggs-per-user') {
503
- const agentId = transportConfig.agentId || hostOptions.agentId;
504
- if (!agentId) {
505
- throw new Error('ziggs-per-user MCP transport requires an agentId to impersonate');
506
- }
507
- if (!toolSchema || toolSchema.length === 0) {
508
- throw new Error('ziggs-per-user MCP transport requires toolSchema (no boot connection to discover tools)');
509
- }
510
- const tools = capTools(toolSchema as McpTool[], agentId, maxTools, extraTools.length, allowedTools);
511
-
512
- const clients = new Map<string, Promise<Client>>();
513
- const clientFor = (
514
- userId: string,
515
- onBehalfOfAgreementId?: string,
516
- ): Promise<Client> => {
517
- // Key by user + agreement: the gateway may resolve a different end-user
518
- // per agreement (delegation), so sessions must not be shared across them.
519
- const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}`;
520
- let p = clients.get(cacheKey);
521
- if (!p) {
522
- p = (async () => {
523
- const target = resolvePerUserGatewayTarget(transportConfig, agentId, userId, onBehalfOfAgreementId);
524
- const transport = new StreamableHTTPClientTransport(target.url, {
525
- requestInit: { headers: target.headers },
526
- fetch: wrapGatewayFetch(),
527
- });
528
- const c = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
529
- await c.connect(transport);
530
- return c;
531
- })().catch((err: unknown) => {
532
- // Don't cache a failed connect (e.g. NO_USER_CONNECTION before the
533
- // user consents) — the next turn must be able to retry.
534
- clients.delete(cacheKey);
535
- throw err;
536
- });
537
- clients.set(cacheKey, p);
538
- }
539
- return p;
540
- };
541
-
542
- // When the invoking user hasn't connected this server, open a connection
543
- // request targeting THEM (X-On-Behalf-Of-User) so the consent prompt lands
544
- // in their inbox. The backend dedups pending requests, so retrying every
545
- // turn is safe. ZIG-771.
546
- const backendBase = (
547
- transportConfig.baseUrl ||
548
- process.env.BACKEND_URL ||
549
- process.env.ZIGGS_BACKEND_URL ||
550
- 'http://localhost:3000'
551
- ).replace(/\/$/, '');
552
- // App URL for the user-facing connect link (ZIG-790): api.ziggsai.com → ziggsai.com.
553
- const appUrl = backendBase.includes('localhost')
554
- ? 'http://localhost:1234'
555
- : backendBase.replace(/\/\/api\./, '//');
556
- const serverLabel = transportConfig.serverUrl
557
- ? new URL(transportConfig.serverUrl).host
558
- : 'the external service';
559
- // Open a connection request for this user; returns its id so the caller can
560
- // deep-link the user straight to it on the Connections page (ZIG-790).
561
- const triggerConnectionRequest = async (userId: string): Promise<string | null> => {
562
- if (!transportConfig.serverUrl) return null;
563
- const operatorKey = transportConfig.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
564
- const res = await fetch(`${backendBase}/connections/mcp/requests`, {
565
- method: 'POST',
566
- headers: {
567
- 'content-type': 'application/json',
568
- authorization: `Bearer ${operatorKey}`,
569
- 'x-agent-id': agentId,
570
- 'x-on-behalf-of-user': userId,
571
- },
572
- body: JSON.stringify({
573
- serverUrl: transportConfig.serverUrl,
574
- tools: tools.map(t => t.name),
575
- }),
576
- });
577
- const body = (await res.json().catch(() => null)) as { request?: { requestId?: string } } | null;
578
- return body?.request?.requestId ?? null;
579
- };
580
-
581
- const caller: McpToolCaller = async (name, args, context) => {
582
- const userId = typeof context.senderId === 'string' ? context.senderId : undefined;
583
- if (!userId) {
584
- throw new Error('per-user MCP tool call requires an invoking user (context.senderId is missing)');
585
- }
586
- // ZIG-831: for a delegated wake, senderId is the delegating agent, not the
587
- // connection-owning end-user; pass the agreement so the gateway resolves
588
- // the delegation root's user.
589
- const onBehalfOfAgreementId =
590
- typeof context.onBehalfOfAgreementId === 'string' ? context.onBehalfOfAgreementId : undefined;
591
- // ZIG-832 (B): per-user resolution is intermittently flaky even when the
592
- // connection is valid and resolvable — the same on-behalf call fails with
593
- // NO_USER_CONNECTION on one wake and succeeds on the next. clientFor never
594
- // caches a failed connect, so each attempt re-resolves from scratch. Retry
595
- // a few times (with backoff) before surfacing the connect prompt, so a
596
- // transient miss no longer fails the whole fetch step.
597
- const MAX_ATTEMPTS = 3;
598
- let lastErr: unknown;
599
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
600
- try {
601
- const c = await clientFor(userId, onBehalfOfAgreementId);
602
- return (await c.callTool({ name, arguments: args })) as {
603
- isError?: boolean;
604
- content: McpContent[];
605
- };
606
- } catch (err) {
607
- lastErr = err;
608
- const resolutionMiss =
609
- err instanceof ZiggsGatewayError && err.code === 'NO_USER_CONNECTION';
610
- const transient = resolutionMiss || !(err instanceof ZiggsGatewayError);
611
- if (attempt < MAX_ATTEMPTS && transient) {
612
- console.warn(
613
- `[McpAdapter] per-user ${name} attempt ${attempt}/${MAX_ATTEMPTS} failed (${(err as Error).message}); retrying with a fresh connection`,
614
- );
615
- await new Promise((r) => setTimeout(r, 500 * attempt));
616
- continue;
617
- }
618
- break;
619
- }
620
- }
621
- if (
622
- lastErr instanceof ZiggsGatewayError &&
623
- lastErr.code === 'NO_USER_CONNECTION' &&
624
- transportConfig.serverUrl
625
- ) {
626
- const requestId = await triggerConnectionRequest(userId).catch(() => null);
627
- const link = `${appUrl}/app/settings/connections${requestId ? `?request=${encodeURIComponent(requestId)}` : ''}`;
628
- return {
629
- content: [
630
- {
631
- type: 'text',
632
- text: `ACTION NEEDED: the user has not connected ${serverLabel}. Ask them to approve the connection here: ${link} — then retry.`,
633
- },
634
- ],
635
- };
636
- }
637
- throw lastErr;
638
- };
639
-
640
- const host = new AgentHost({
641
- ...hostOptions,
642
- workflow: workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', tools),
643
- tools: [...mapMcpTools(caller, tools), ...extraTools],
644
- });
645
- return {
646
- host,
647
- closeMcp: async () => {
648
- await Promise.all(
649
- [...clients.values()].map(p => p.then(c => c.close()).catch(() => {})),
650
- );
651
- },
652
- toolNames: tools.map(t => t.name),
653
- };
654
- }
655
-
656
- // ── Single-client path (stdio / http / brokered ziggs): discover tools at boot.
657
- const transport = buildTransport(transportConfig, { agentId: hostOptions.agentId });
658
- const client = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
659
- await client.connect(transport);
660
-
661
- const rawTools = capTools((await client.listTools()).tools as McpTool[], hostOptions.agentId, maxTools, extraTools.length, allowedTools);
662
- const caller: McpToolCaller = (name, args) =>
663
- client.callTool({ name, arguments: args }) as Promise<{ isError?: boolean; content: McpContent[] }>;
664
- const mcpTools = mapMcpTools(caller, rawTools);
665
-
666
- const resolvedWorkflow = workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', rawTools);
667
- const host = new AgentHost({
668
- ...hostOptions,
669
- workflow: resolvedWorkflow,
670
- tools: [...mcpTools, ...extraTools],
671
- });
672
-
673
- return {
674
- host,
675
- closeMcp: () => client.close(),
676
- toolNames: rawTools.map((t: McpTool) => t.name),
677
- };
678
- }