@poncho-ai/harness 0.33.1 → 0.34.0

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.33.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.34.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 335.08 KB
12
- ESM ⚡️ Build success in 167ms
11
+ ESM dist/index.js 336.31 KB
12
+ ESM ⚡️ Build success in 164ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 7028ms
15
- DTS dist/index.d.ts 33.55 KB
14
+ DTS ⚡️ Build success in 7760ms
15
+ DTS dist/index.d.ts 33.99 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3f096f2`](https://github.com/cesr/poncho-ai/commit/3f096f28b9ab797b52f1b725778976929156cce9) Thanks [@cesr](https://github.com/cesr)! - fix: scope MCP tools to skills via server-level claiming
8
+
9
+ MCP tools from configured servers are now globally available by default. When a skill claims any tool from a server via `allowed-tools`, the entire server becomes skill-managed — its tools are only available when the claiming skill is active (or declared in AGENT.md `allowed-tools`).
10
+
3
11
  ## 0.33.1
4
12
 
5
13
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -759,6 +759,14 @@ declare class AgentHarness {
759
759
  private getAgentScriptIntent;
760
760
  private getAgentMcpApprovalPatterns;
761
761
  private getAgentScriptApprovalPatterns;
762
+ /**
763
+ * Return the set of MCP server names that have at least one tool claimed by
764
+ * any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
765
+ * server, the entire server is considered "skill-managed" — none of its tools
766
+ * are auto-exposed globally; only explicitly declared tools become available
767
+ * (via agent-level allowed-tools or active skill allowed-tools).
768
+ */
769
+ private getSkillManagedMcpServers;
762
770
  private getRequestedMcpPatterns;
763
771
  private getRequestedScriptPatterns;
764
772
  private getRequestedMcpApprovalPatterns;
package/dist/index.js CHANGED
@@ -882,6 +882,7 @@ Connect your Poncho agent to messaging platforms so it responds to @mentions.
882
882
  1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app "From scratch"
883
883
  2. Under **OAuth & Permissions**, add these Bot Token Scopes:
884
884
  - \`app_mentions:read\`
885
+ - \`channels:history\` (needed to fetch thread context when mentioned in a reply)
885
886
  - \`chat:write\`
886
887
  - \`reactions:write\`
887
888
  3. Under **Event Subscriptions**, enable events:
@@ -6301,6 +6302,25 @@ var AgentHarness = class _AgentHarness {
6301
6302
  getAgentScriptApprovalPatterns() {
6302
6303
  return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
6303
6304
  }
6305
+ /**
6306
+ * Return the set of MCP server names that have at least one tool claimed by
6307
+ * any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
6308
+ * server, the entire server is considered "skill-managed" — none of its tools
6309
+ * are auto-exposed globally; only explicitly declared tools become available
6310
+ * (via agent-level allowed-tools or active skill allowed-tools).
6311
+ */
6312
+ getSkillManagedMcpServers() {
6313
+ const servers = /* @__PURE__ */ new Set();
6314
+ for (const skill of this.loadedSkills) {
6315
+ for (const pattern of skill.allowedTools.mcp) {
6316
+ const slash = pattern.indexOf("/");
6317
+ if (slash > 0) {
6318
+ servers.add(pattern.slice(0, slash));
6319
+ }
6320
+ }
6321
+ }
6322
+ return servers;
6323
+ }
6304
6324
  getRequestedMcpPatterns() {
6305
6325
  const patterns = new Set(this.getAgentMcpIntent());
6306
6326
  for (const skillName of this.activeSkillNames) {
@@ -6312,6 +6332,17 @@ var AgentHarness = class _AgentHarness {
6312
6332
  patterns.add(pattern);
6313
6333
  }
6314
6334
  }
6335
+ if (this.mcpBridge) {
6336
+ const managedServers = this.getSkillManagedMcpServers();
6337
+ const discoveredTools = this.mcpBridge.listDiscoveredTools();
6338
+ for (const toolName of discoveredTools) {
6339
+ const slash = toolName.indexOf("/");
6340
+ const serverName = slash > 0 ? toolName.slice(0, slash) : toolName;
6341
+ if (!managedServers.has(serverName)) {
6342
+ patterns.add(toolName);
6343
+ }
6344
+ }
6345
+ }
6315
6346
  return [...patterns];
6316
6347
  }
6317
6348
  getRequestedScriptPatterns() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -995,8 +995,30 @@ export class AgentHarness {
995
995
  return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
996
996
  }
997
997
 
998
+ /**
999
+ * Return the set of MCP server names that have at least one tool claimed by
1000
+ * any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
1001
+ * server, the entire server is considered "skill-managed" — none of its tools
1002
+ * are auto-exposed globally; only explicitly declared tools become available
1003
+ * (via agent-level allowed-tools or active skill allowed-tools).
1004
+ */
1005
+ private getSkillManagedMcpServers(): Set<string> {
1006
+ const servers = new Set<string>();
1007
+ for (const skill of this.loadedSkills) {
1008
+ for (const pattern of skill.allowedTools.mcp) {
1009
+ const slash = pattern.indexOf("/");
1010
+ if (slash > 0) {
1011
+ servers.add(pattern.slice(0, slash));
1012
+ }
1013
+ }
1014
+ }
1015
+ return servers;
1016
+ }
1017
+
998
1018
  private getRequestedMcpPatterns(): string[] {
999
1019
  const patterns = new Set<string>(this.getAgentMcpIntent());
1020
+
1021
+ // Add patterns from active skills.
1000
1022
  for (const skillName of this.activeSkillNames) {
1001
1023
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
1002
1024
  if (!skill) {
@@ -1006,6 +1028,26 @@ export class AgentHarness {
1006
1028
  patterns.add(pattern);
1007
1029
  }
1008
1030
  }
1031
+
1032
+ // MCP servers whose tools are NOT claimed by any skill are "unmanaged" —
1033
+ // all their discovered tools are globally available so that configuring a
1034
+ // server in poncho.config.js makes its tools accessible by default.
1035
+ //
1036
+ // Once ANY skill claims tools from a server (even a single tool), that
1037
+ // server becomes "skill-managed" and ALL of its tools require explicit
1038
+ // declaration (agent-level or active-skill) to be available.
1039
+ if (this.mcpBridge) {
1040
+ const managedServers = this.getSkillManagedMcpServers();
1041
+ const discoveredTools = this.mcpBridge.listDiscoveredTools();
1042
+ for (const toolName of discoveredTools) {
1043
+ const slash = toolName.indexOf("/");
1044
+ const serverName = slash > 0 ? toolName.slice(0, slash) : toolName;
1045
+ if (!managedServers.has(serverName)) {
1046
+ patterns.add(toolName);
1047
+ }
1048
+ }
1049
+ }
1050
+
1009
1051
  return [...patterns];
1010
1052
  }
1011
1053
 
@@ -1037,6 +1037,360 @@ allowed-tools:
1037
1037
  await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
1038
1038
  });
1039
1039
 
1040
+ it("unclaimed MCP tools are globally available without allowed-tools declaration", async () => {
1041
+ process.env.LINEAR_TOKEN = "token-123";
1042
+ const mcpServer = createServer(async (req, res) => {
1043
+ if (req.method === "DELETE") {
1044
+ res.statusCode = 200;
1045
+ res.end();
1046
+ return;
1047
+ }
1048
+ const chunks: Buffer[] = [];
1049
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
1050
+ const body = Buffer.concat(chunks).toString("utf8");
1051
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
1052
+ if (payload.method === "initialize") {
1053
+ res.setHeader("Content-Type", "application/json");
1054
+ res.setHeader("Mcp-Session-Id", "sess");
1055
+ res.end(
1056
+ JSON.stringify({
1057
+ jsonrpc: "2.0",
1058
+ id: payload.id,
1059
+ result: {
1060
+ protocolVersion: "2025-03-26",
1061
+ capabilities: { tools: { listChanged: true } },
1062
+ serverInfo: { name: "remote", version: "1.0.0" },
1063
+ },
1064
+ }),
1065
+ );
1066
+ return;
1067
+ }
1068
+ if (payload.method === "notifications/initialized") {
1069
+ res.statusCode = 202;
1070
+ res.end();
1071
+ return;
1072
+ }
1073
+ if (payload.method === "tools/list") {
1074
+ res.setHeader("Content-Type", "application/json");
1075
+ res.end(
1076
+ JSON.stringify({
1077
+ jsonrpc: "2.0",
1078
+ id: payload.id,
1079
+ result: {
1080
+ tools: [
1081
+ { name: "list_issues", inputSchema: { type: "object", properties: {} } },
1082
+ { name: "save_issue", inputSchema: { type: "object", properties: {} } },
1083
+ ],
1084
+ },
1085
+ }),
1086
+ );
1087
+ return;
1088
+ }
1089
+ res.statusCode = 404;
1090
+ res.end();
1091
+ });
1092
+ await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
1093
+ const address = mcpServer.address();
1094
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
1095
+ const dir = await mkdtemp(join(tmpdir(), "poncho-harness-unclaimed-mcp-"));
1096
+ // AGENT.md with no allowed-tools and no skills — MCP tools should be globally available
1097
+ await writeFile(
1098
+ join(dir, "AGENT.md"),
1099
+ `---
1100
+ name: unclaimed-mcp-agent
1101
+ model:
1102
+ provider: anthropic
1103
+ name: claude-opus-4-5
1104
+ ---
1105
+
1106
+ # Agent with unclaimed MCP tools
1107
+ `,
1108
+ "utf8",
1109
+ );
1110
+ await writeFile(
1111
+ join(dir, "poncho.config.js"),
1112
+ `export default {
1113
+ mcp: [
1114
+ {
1115
+ name: "linear",
1116
+ url: "http://127.0.0.1:${address.port}/mcp",
1117
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
1118
+ }
1119
+ ]
1120
+ };
1121
+ `,
1122
+ "utf8",
1123
+ );
1124
+ const harness = new AgentHarness({ workingDir: dir });
1125
+ await harness.initialize();
1126
+ const toolNames = () => harness.listTools().map((t) => t.name);
1127
+ // Unclaimed tools should be globally available
1128
+ expect(toolNames()).toContain("linear/list_issues");
1129
+ expect(toolNames()).toContain("linear/save_issue");
1130
+ await harness.shutdown();
1131
+ await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
1132
+ });
1133
+
1134
+ it("claiming any tool from a server scopes the entire server", async () => {
1135
+ process.env.LINEAR_TOKEN = "token-123";
1136
+ const mcpServer = createServer(async (req, res) => {
1137
+ if (req.method === "DELETE") {
1138
+ res.statusCode = 200;
1139
+ res.end();
1140
+ return;
1141
+ }
1142
+ const chunks: Buffer[] = [];
1143
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
1144
+ const body = Buffer.concat(chunks).toString("utf8");
1145
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
1146
+ if (payload.method === "initialize") {
1147
+ res.setHeader("Content-Type", "application/json");
1148
+ res.setHeader("Mcp-Session-Id", "sess");
1149
+ res.end(
1150
+ JSON.stringify({
1151
+ jsonrpc: "2.0",
1152
+ id: payload.id,
1153
+ result: {
1154
+ protocolVersion: "2025-03-26",
1155
+ capabilities: { tools: { listChanged: true } },
1156
+ serverInfo: { name: "remote", version: "1.0.0" },
1157
+ },
1158
+ }),
1159
+ );
1160
+ return;
1161
+ }
1162
+ if (payload.method === "notifications/initialized") {
1163
+ res.statusCode = 202;
1164
+ res.end();
1165
+ return;
1166
+ }
1167
+ if (payload.method === "tools/list") {
1168
+ res.setHeader("Content-Type", "application/json");
1169
+ res.end(
1170
+ JSON.stringify({
1171
+ jsonrpc: "2.0",
1172
+ id: payload.id,
1173
+ result: {
1174
+ tools: [
1175
+ { name: "list_issues", inputSchema: { type: "object", properties: {} } },
1176
+ { name: "save_issue", inputSchema: { type: "object", properties: {} } },
1177
+ { name: "other_tool", inputSchema: { type: "object", properties: {} } },
1178
+ ],
1179
+ },
1180
+ }),
1181
+ );
1182
+ return;
1183
+ }
1184
+ if (payload.method === "tools/call") {
1185
+ res.setHeader("Content-Type", "application/json");
1186
+ res.end(
1187
+ JSON.stringify({
1188
+ jsonrpc: "2.0",
1189
+ id: payload.id,
1190
+ result: { result: { ok: true } },
1191
+ }),
1192
+ );
1193
+ return;
1194
+ }
1195
+ res.statusCode = 404;
1196
+ res.end();
1197
+ });
1198
+ await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
1199
+ const address = mcpServer.address();
1200
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
1201
+ const dir = await mkdtemp(join(tmpdir(), "poncho-harness-server-scoped-"));
1202
+ await writeFile(
1203
+ join(dir, "AGENT.md"),
1204
+ `---
1205
+ name: server-scoped-agent
1206
+ model:
1207
+ provider: anthropic
1208
+ name: claude-opus-4-5
1209
+ ---
1210
+
1211
+ # Server Scoped Agent
1212
+ `,
1213
+ "utf8",
1214
+ );
1215
+ await writeFile(
1216
+ join(dir, "poncho.config.js"),
1217
+ `export default {
1218
+ mcp: [
1219
+ {
1220
+ name: "remote",
1221
+ url: "http://127.0.0.1:${address.port}/mcp",
1222
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
1223
+ }
1224
+ ]
1225
+ };
1226
+ `,
1227
+ "utf8",
1228
+ );
1229
+ // Skill claims only 2 of the 3 tools from "remote" server
1230
+ await mkdir(join(dir, "skills", "linear"), { recursive: true });
1231
+ await writeFile(
1232
+ join(dir, "skills", "linear", "SKILL.md"),
1233
+ `---
1234
+ name: linear
1235
+ description: Linear issue tracking
1236
+ allowed-tools:
1237
+ - mcp:remote/list_issues
1238
+ - mcp:remote/save_issue
1239
+ ---
1240
+ # Linear Skill
1241
+ `,
1242
+ "utf8",
1243
+ );
1244
+ const harness = new AgentHarness({ workingDir: dir });
1245
+ await harness.initialize();
1246
+ const toolNames = () => harness.listTools().map((t) => t.name);
1247
+
1248
+ // Before activation: entire server is skill-managed, so ALL tools are hidden
1249
+ // (even other_tool which the skill didn't explicitly claim)
1250
+ expect(toolNames()).not.toContain("remote/list_issues");
1251
+ expect(toolNames()).not.toContain("remote/save_issue");
1252
+ expect(toolNames()).not.toContain("remote/other_tool");
1253
+
1254
+ // Activate the linear skill — only its declared tools appear
1255
+ const activate = harness.listTools().find((t) => t.name === "activate_skill")!;
1256
+ const deactivate = harness.listTools().find((t) => t.name === "deactivate_skill")!;
1257
+ await activate.handler({ name: "linear" }, {} as any);
1258
+
1259
+ expect(toolNames()).toContain("remote/list_issues");
1260
+ expect(toolNames()).toContain("remote/save_issue");
1261
+ // other_tool is NOT claimed by any active skill, and the server is managed
1262
+ expect(toolNames()).not.toContain("remote/other_tool");
1263
+
1264
+ // Deactivate — all tools from the managed server hidden again
1265
+ await deactivate.handler({ name: "linear" }, {} as any);
1266
+ expect(toolNames()).not.toContain("remote/list_issues");
1267
+ expect(toolNames()).not.toContain("remote/save_issue");
1268
+ expect(toolNames()).not.toContain("remote/other_tool");
1269
+
1270
+ await harness.shutdown();
1271
+ await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
1272
+ });
1273
+
1274
+ it("wildcard skill claim scopes all tools from that server", async () => {
1275
+ process.env.LINEAR_TOKEN = "token-123";
1276
+ const mcpServer = createServer(async (req, res) => {
1277
+ if (req.method === "DELETE") {
1278
+ res.statusCode = 200;
1279
+ res.end();
1280
+ return;
1281
+ }
1282
+ const chunks: Buffer[] = [];
1283
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
1284
+ const body = Buffer.concat(chunks).toString("utf8");
1285
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
1286
+ if (payload.method === "initialize") {
1287
+ res.setHeader("Content-Type", "application/json");
1288
+ res.setHeader("Mcp-Session-Id", "sess");
1289
+ res.end(
1290
+ JSON.stringify({
1291
+ jsonrpc: "2.0",
1292
+ id: payload.id,
1293
+ result: {
1294
+ protocolVersion: "2025-03-26",
1295
+ capabilities: { tools: { listChanged: true } },
1296
+ serverInfo: { name: "linear", version: "1.0.0" },
1297
+ },
1298
+ }),
1299
+ );
1300
+ return;
1301
+ }
1302
+ if (payload.method === "notifications/initialized") {
1303
+ res.statusCode = 202;
1304
+ res.end();
1305
+ return;
1306
+ }
1307
+ if (payload.method === "tools/list") {
1308
+ res.setHeader("Content-Type", "application/json");
1309
+ res.end(
1310
+ JSON.stringify({
1311
+ jsonrpc: "2.0",
1312
+ id: payload.id,
1313
+ result: {
1314
+ tools: [
1315
+ { name: "list_issues", inputSchema: { type: "object", properties: {} } },
1316
+ { name: "save_issue", inputSchema: { type: "object", properties: {} } },
1317
+ { name: "save_comment", inputSchema: { type: "object", properties: {} } },
1318
+ ],
1319
+ },
1320
+ }),
1321
+ );
1322
+ return;
1323
+ }
1324
+ res.statusCode = 404;
1325
+ res.end();
1326
+ });
1327
+ await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
1328
+ const address = mcpServer.address();
1329
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
1330
+ const dir = await mkdtemp(join(tmpdir(), "poncho-harness-wildcard-claim-"));
1331
+ await writeFile(
1332
+ join(dir, "AGENT.md"),
1333
+ `---
1334
+ name: wildcard-agent
1335
+ model:
1336
+ provider: anthropic
1337
+ name: claude-opus-4-5
1338
+ ---
1339
+
1340
+ # Wildcard Agent
1341
+ `,
1342
+ "utf8",
1343
+ );
1344
+ await writeFile(
1345
+ join(dir, "poncho.config.js"),
1346
+ `export default {
1347
+ mcp: [
1348
+ {
1349
+ name: "linear",
1350
+ url: "http://127.0.0.1:${address.port}/mcp",
1351
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
1352
+ }
1353
+ ]
1354
+ };
1355
+ `,
1356
+ "utf8",
1357
+ );
1358
+ // Skill claims all linear tools with wildcard
1359
+ await mkdir(join(dir, "skills", "linear"), { recursive: true });
1360
+ await writeFile(
1361
+ join(dir, "skills", "linear", "SKILL.md"),
1362
+ `---
1363
+ name: linear
1364
+ description: Linear integration
1365
+ allowed-tools:
1366
+ - mcp:linear/*
1367
+ ---
1368
+ # Linear Skill
1369
+ `,
1370
+ "utf8",
1371
+ );
1372
+ const harness = new AgentHarness({ workingDir: dir });
1373
+ await harness.initialize();
1374
+ const toolNames = () => harness.listTools().map((t) => t.name);
1375
+
1376
+ // None of the linear tools should be available (all claimed by wildcard)
1377
+ expect(toolNames()).not.toContain("linear/list_issues");
1378
+ expect(toolNames()).not.toContain("linear/save_issue");
1379
+ expect(toolNames()).not.toContain("linear/save_comment");
1380
+
1381
+ // Activate the skill
1382
+ const activate = harness.listTools().find((t) => t.name === "activate_skill")!;
1383
+ await activate.handler({ name: "linear" }, {} as any);
1384
+
1385
+ // All linear tools now available
1386
+ expect(toolNames()).toContain("linear/list_issues");
1387
+ expect(toolNames()).toContain("linear/save_issue");
1388
+ expect(toolNames()).toContain("linear/save_comment");
1389
+
1390
+ await harness.shutdown();
1391
+ await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
1392
+ });
1393
+
1040
1394
  it("supports flat tool access config format", async () => {
1041
1395
  const dir = await mkdtemp(join(tmpdir(), "poncho-harness-flat-tool-access-"));
1042
1396
  await writeFile(