@poncho-ai/harness 0.17.0 → 0.18.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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +39 -23
- package/package.json +2 -2
- package/src/harness.ts +21 -24
- package/src/mcp.ts +11 -1
- package/src/state.ts +17 -10
- package/test/mcp.test.ts +83 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.18.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[32m196.
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m196.78 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 133ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 6813ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m24.01 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce) Thanks [@cesr](https://github.com/cesr)! - Add MCP custom headers support, tool:generating streaming feedback, and cross-owner subagent recovery
|
|
8
|
+
- **MCP custom headers**: `poncho mcp add --header "Name: value"` and `headers` config field let servers like Arcade receive extra HTTP headers alongside bearer auth.
|
|
9
|
+
- **tool:generating event**: the harness now emits `tool:generating` events when the model begins writing tool-call arguments, so the web UI shows real-time "preparing <tool>" feedback instead of appearing stuck during large tool calls.
|
|
10
|
+
- **Subagent recovery**: `list`/`listSummaries` accept optional `ownerId` so stale-subagent recovery on server restart scans across all owners.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [[`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce)]:
|
|
15
|
+
- @poncho-ai/sdk@1.3.0
|
|
16
|
+
|
|
3
17
|
## 0.17.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1306,16 +1306,19 @@ var StreamableHttpMcpRpcClient = class {
|
|
|
1306
1306
|
endpoint;
|
|
1307
1307
|
timeoutMs;
|
|
1308
1308
|
bearerToken;
|
|
1309
|
+
customHeaders;
|
|
1309
1310
|
idCounter = 1;
|
|
1310
1311
|
initialized = false;
|
|
1311
1312
|
sessionId;
|
|
1312
|
-
constructor(endpoint, timeoutMs = 1e4, bearerToken) {
|
|
1313
|
+
constructor(endpoint, timeoutMs = 1e4, bearerToken, customHeaders) {
|
|
1313
1314
|
this.endpoint = endpoint;
|
|
1314
1315
|
this.timeoutMs = timeoutMs;
|
|
1315
1316
|
this.bearerToken = bearerToken;
|
|
1317
|
+
this.customHeaders = customHeaders ?? {};
|
|
1316
1318
|
}
|
|
1317
1319
|
buildHeaders(accept) {
|
|
1318
1320
|
const headers = {
|
|
1321
|
+
...this.customHeaders,
|
|
1319
1322
|
"Content-Type": "application/json",
|
|
1320
1323
|
Accept: accept
|
|
1321
1324
|
};
|
|
@@ -1596,7 +1599,8 @@ var LocalMcpBridge = class {
|
|
|
1596
1599
|
new StreamableHttpMcpRpcClient(
|
|
1597
1600
|
server.url,
|
|
1598
1601
|
server.timeoutMs ?? 1e4,
|
|
1599
|
-
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0
|
|
1602
|
+
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0,
|
|
1603
|
+
server.headers
|
|
1600
1604
|
)
|
|
1601
1605
|
);
|
|
1602
1606
|
}
|
|
@@ -4063,7 +4067,7 @@ ${textContent}` };
|
|
|
4063
4067
|
let chunkCount = 0;
|
|
4064
4068
|
const hasRunTimeout = timeoutMs > 0;
|
|
4065
4069
|
const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
|
|
4066
|
-
const
|
|
4070
|
+
const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
|
|
4067
4071
|
try {
|
|
4068
4072
|
while (true) {
|
|
4069
4073
|
if (isCancelled()) {
|
|
@@ -4089,20 +4093,20 @@ ${textContent}` };
|
|
|
4089
4093
|
}
|
|
4090
4094
|
const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
|
|
4091
4095
|
const timeout = chunkCount === 0 ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS) : hasRunTimeout ? remaining : 0;
|
|
4092
|
-
let
|
|
4096
|
+
let nextPart;
|
|
4093
4097
|
if (timeout <= 0 && chunkCount > 0) {
|
|
4094
|
-
|
|
4098
|
+
nextPart = await fullStreamIterator.next();
|
|
4095
4099
|
} else {
|
|
4096
4100
|
let timer;
|
|
4097
|
-
|
|
4098
|
-
|
|
4101
|
+
nextPart = await Promise.race([
|
|
4102
|
+
fullStreamIterator.next(),
|
|
4099
4103
|
new Promise((resolve10) => {
|
|
4100
4104
|
timer = setTimeout(() => resolve10(null), timeout);
|
|
4101
4105
|
})
|
|
4102
4106
|
]);
|
|
4103
4107
|
clearTimeout(timer);
|
|
4104
4108
|
}
|
|
4105
|
-
if (
|
|
4109
|
+
if (nextPart === null) {
|
|
4106
4110
|
const isFirstChunk = chunkCount === 0;
|
|
4107
4111
|
console.error(
|
|
4108
4112
|
`[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`
|
|
@@ -4120,13 +4124,19 @@ ${textContent}` };
|
|
|
4120
4124
|
});
|
|
4121
4125
|
return;
|
|
4122
4126
|
}
|
|
4123
|
-
if (
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
+
if (nextPart.done) break;
|
|
4128
|
+
const part = nextPart.value;
|
|
4129
|
+
if (part.type === "text-delta") {
|
|
4130
|
+
chunkCount += 1;
|
|
4131
|
+
fullText += part.text;
|
|
4132
|
+
yield pushEvent({ type: "model:chunk", content: part.text });
|
|
4133
|
+
} else if (part.type === "tool-input-start") {
|
|
4134
|
+
chunkCount += 1;
|
|
4135
|
+
yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
|
|
4136
|
+
}
|
|
4127
4137
|
}
|
|
4128
4138
|
} finally {
|
|
4129
|
-
|
|
4139
|
+
fullStreamIterator.return?.(void 0)?.catch?.(() => {
|
|
4130
4140
|
});
|
|
4131
4141
|
}
|
|
4132
4142
|
if (isCancelled()) {
|
|
@@ -4693,13 +4703,13 @@ var InMemoryConversationStore = class {
|
|
|
4693
4703
|
}
|
|
4694
4704
|
}
|
|
4695
4705
|
}
|
|
4696
|
-
async list(ownerId
|
|
4706
|
+
async list(ownerId) {
|
|
4697
4707
|
this.purgeExpired();
|
|
4698
|
-
return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4708
|
+
return Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4699
4709
|
}
|
|
4700
|
-
async listSummaries(ownerId
|
|
4710
|
+
async listSummaries(ownerId) {
|
|
4701
4711
|
this.purgeExpired();
|
|
4702
|
-
return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4712
|
+
return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4703
4713
|
conversationId: c.conversationId,
|
|
4704
4714
|
title: c.title,
|
|
4705
4715
|
updatedAt: c.updatedAt,
|
|
@@ -4875,9 +4885,9 @@ var FileConversationStore = class {
|
|
|
4875
4885
|
});
|
|
4876
4886
|
await this.writing;
|
|
4877
4887
|
}
|
|
4878
|
-
async list(ownerId
|
|
4888
|
+
async list(ownerId) {
|
|
4879
4889
|
await this.ensureLoaded();
|
|
4880
|
-
const summaries = Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4890
|
+
const summaries = Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4881
4891
|
const conversations = [];
|
|
4882
4892
|
for (const summary of summaries) {
|
|
4883
4893
|
const loaded = await this.readConversationFile(summary.fileName);
|
|
@@ -4887,9 +4897,9 @@ var FileConversationStore = class {
|
|
|
4887
4897
|
}
|
|
4888
4898
|
return conversations;
|
|
4889
4899
|
}
|
|
4890
|
-
async listSummaries(ownerId
|
|
4900
|
+
async listSummaries(ownerId) {
|
|
4891
4901
|
await this.ensureLoaded();
|
|
4892
|
-
return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4902
|
+
return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
|
|
4893
4903
|
conversationId: c.conversationId,
|
|
4894
4904
|
title: c.title,
|
|
4895
4905
|
updatedAt: c.updatedAt,
|
|
@@ -5115,11 +5125,14 @@ var KeyValueConversationStoreBase = class {
|
|
|
5115
5125
|
return void 0;
|
|
5116
5126
|
}
|
|
5117
5127
|
}
|
|
5118
|
-
async list(ownerId
|
|
5128
|
+
async list(ownerId) {
|
|
5119
5129
|
const kv = await this.client();
|
|
5120
5130
|
if (!kv) {
|
|
5121
5131
|
return await this.memoryFallback.list(ownerId);
|
|
5122
5132
|
}
|
|
5133
|
+
if (!ownerId) {
|
|
5134
|
+
return [];
|
|
5135
|
+
}
|
|
5123
5136
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
5124
5137
|
const conversations = [];
|
|
5125
5138
|
for (const id of ids) {
|
|
@@ -5134,11 +5147,14 @@ var KeyValueConversationStoreBase = class {
|
|
|
5134
5147
|
}
|
|
5135
5148
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
5136
5149
|
}
|
|
5137
|
-
async listSummaries(ownerId
|
|
5150
|
+
async listSummaries(ownerId) {
|
|
5138
5151
|
const kv = await this.client();
|
|
5139
5152
|
if (!kv) {
|
|
5140
5153
|
return await this.memoryFallback.listSummaries(ownerId);
|
|
5141
5154
|
}
|
|
5155
|
+
if (!ownerId) {
|
|
5156
|
+
return [];
|
|
5157
|
+
}
|
|
5142
5158
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
5143
5159
|
const summaries = [];
|
|
5144
5160
|
for (const id of ids) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"redis": "^5.10.0",
|
|
32
32
|
"yaml": "^2.4.0",
|
|
33
33
|
"zod": "^3.22.0",
|
|
34
|
-
"@poncho-ai/sdk": "1.
|
|
34
|
+
"@poncho-ai/sdk": "1.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/mustache": "^4.2.6",
|
package/src/harness.ts
CHANGED
|
@@ -1682,15 +1682,14 @@ ${boundedMainMemory.trim()}`
|
|
|
1682
1682
|
isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
|
|
1683
1683
|
},
|
|
1684
1684
|
});
|
|
1685
|
-
// Stream
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
// each next() call against the remaining time budget.
|
|
1685
|
+
// Stream full response — use fullStream to get visibility into
|
|
1686
|
+
// tool-call generation (tool-input-start) in addition to text deltas.
|
|
1687
|
+
// Enforce overall run timeout per part.
|
|
1689
1688
|
let fullText = "";
|
|
1690
1689
|
let chunkCount = 0;
|
|
1691
1690
|
const hasRunTimeout = timeoutMs > 0;
|
|
1692
1691
|
const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
|
|
1693
|
-
const
|
|
1692
|
+
const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
|
|
1694
1693
|
try {
|
|
1695
1694
|
while (true) {
|
|
1696
1695
|
if (isCancelled()) {
|
|
@@ -1714,21 +1713,17 @@ ${boundedMainMemory.trim()}`
|
|
|
1714
1713
|
return;
|
|
1715
1714
|
}
|
|
1716
1715
|
}
|
|
1717
|
-
// Use a shorter timeout for the first chunk to detect
|
|
1718
|
-
// non-responsive models quickly instead of waiting minutes.
|
|
1719
|
-
// When no run timeout is set, only the first chunk is time-bounded.
|
|
1720
1716
|
const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
|
|
1721
1717
|
const timeout = chunkCount === 0
|
|
1722
1718
|
? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS)
|
|
1723
1719
|
: hasRunTimeout ? remaining : 0;
|
|
1724
|
-
let
|
|
1720
|
+
let nextPart: IteratorResult<(typeof result.fullStream) extends AsyncIterable<infer T> ? T : never> | null;
|
|
1725
1721
|
if (timeout <= 0 && chunkCount > 0) {
|
|
1726
|
-
|
|
1727
|
-
nextChunk = await textIterator.next();
|
|
1722
|
+
nextPart = await fullStreamIterator.next();
|
|
1728
1723
|
} else {
|
|
1729
1724
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1730
|
-
|
|
1731
|
-
|
|
1725
|
+
nextPart = await Promise.race([
|
|
1726
|
+
fullStreamIterator.next(),
|
|
1732
1727
|
new Promise<null>((resolve) => {
|
|
1733
1728
|
timer = setTimeout(() => resolve(null), timeout);
|
|
1734
1729
|
}),
|
|
@@ -1736,16 +1731,14 @@ ${boundedMainMemory.trim()}`
|
|
|
1736
1731
|
clearTimeout(timer);
|
|
1737
1732
|
}
|
|
1738
1733
|
|
|
1739
|
-
if (
|
|
1734
|
+
if (nextPart === null) {
|
|
1740
1735
|
const isFirstChunk = chunkCount === 0;
|
|
1741
1736
|
console.error(
|
|
1742
1737
|
`[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`,
|
|
1743
1738
|
);
|
|
1744
1739
|
if (isFirstChunk) {
|
|
1745
|
-
// Throw so the step-level retry logic can handle it.
|
|
1746
1740
|
throw new FirstChunkTimeoutError(modelName, FIRST_CHUNK_TIMEOUT_MS);
|
|
1747
1741
|
}
|
|
1748
|
-
// Mid-stream timeout: not retryable (partial response would be lost)
|
|
1749
1742
|
yield pushEvent({
|
|
1750
1743
|
type: "run:error",
|
|
1751
1744
|
runId,
|
|
@@ -1757,14 +1750,20 @@ ${boundedMainMemory.trim()}`
|
|
|
1757
1750
|
return;
|
|
1758
1751
|
}
|
|
1759
1752
|
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1753
|
+
if (nextPart.done) break;
|
|
1754
|
+
const part = nextPart.value;
|
|
1755
|
+
|
|
1756
|
+
if (part.type === "text-delta") {
|
|
1757
|
+
chunkCount += 1;
|
|
1758
|
+
fullText += part.text;
|
|
1759
|
+
yield pushEvent({ type: "model:chunk", content: part.text });
|
|
1760
|
+
} else if (part.type === "tool-input-start") {
|
|
1761
|
+
chunkCount += 1;
|
|
1762
|
+
yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
|
|
1763
|
+
}
|
|
1764
1764
|
}
|
|
1765
1765
|
} finally {
|
|
1766
|
-
|
|
1767
|
-
textIterator.return?.(undefined)?.catch?.(() => {});
|
|
1766
|
+
fullStreamIterator.return?.(undefined)?.catch?.(() => {});
|
|
1768
1767
|
}
|
|
1769
1768
|
|
|
1770
1769
|
if (isCancelled()) {
|
|
@@ -1773,8 +1772,6 @@ ${boundedMainMemory.trim()}`
|
|
|
1773
1772
|
}
|
|
1774
1773
|
|
|
1775
1774
|
// Check finish reason for error / abnormal completions.
|
|
1776
|
-
// textStream silently swallows model-level errors – they only
|
|
1777
|
-
// surface through finishReason (or fullStream, which we don't use).
|
|
1778
1775
|
const finishReason = await result.finishReason;
|
|
1779
1776
|
|
|
1780
1777
|
if (finishReason === "error") {
|
package/src/mcp.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface RemoteMcpServerConfig {
|
|
|
12
12
|
type: "bearer";
|
|
13
13
|
tokenEnv?: string;
|
|
14
14
|
};
|
|
15
|
+
headers?: Record<string, string>;
|
|
15
16
|
timeoutMs?: number;
|
|
16
17
|
reconnectAttempts?: number;
|
|
17
18
|
reconnectDelayMs?: number;
|
|
@@ -46,18 +47,26 @@ class StreamableHttpMcpRpcClient implements McpRpcClient {
|
|
|
46
47
|
private readonly endpoint: string;
|
|
47
48
|
private readonly timeoutMs: number;
|
|
48
49
|
private readonly bearerToken?: string;
|
|
50
|
+
private readonly customHeaders: Record<string, string>;
|
|
49
51
|
private idCounter = 1;
|
|
50
52
|
private initialized = false;
|
|
51
53
|
private sessionId?: string;
|
|
52
54
|
|
|
53
|
-
constructor(
|
|
55
|
+
constructor(
|
|
56
|
+
endpoint: string,
|
|
57
|
+
timeoutMs = 10_000,
|
|
58
|
+
bearerToken?: string,
|
|
59
|
+
customHeaders?: Record<string, string>,
|
|
60
|
+
) {
|
|
54
61
|
this.endpoint = endpoint;
|
|
55
62
|
this.timeoutMs = timeoutMs;
|
|
56
63
|
this.bearerToken = bearerToken;
|
|
64
|
+
this.customHeaders = customHeaders ?? {};
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
private buildHeaders(accept: string): Record<string, string> {
|
|
60
68
|
const headers: Record<string, string> = {
|
|
69
|
+
...this.customHeaders,
|
|
61
70
|
"Content-Type": "application/json",
|
|
62
71
|
Accept: accept,
|
|
63
72
|
};
|
|
@@ -367,6 +376,7 @@ export class LocalMcpBridge {
|
|
|
367
376
|
server.url,
|
|
368
377
|
server.timeoutMs ?? 10_000,
|
|
369
378
|
server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : undefined,
|
|
379
|
+
server.headers,
|
|
370
380
|
),
|
|
371
381
|
);
|
|
372
382
|
}
|
package/src/state.ts
CHANGED
|
@@ -223,17 +223,17 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
async list(ownerId
|
|
226
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
227
227
|
this.purgeExpired();
|
|
228
228
|
return Array.from(this.conversations.values())
|
|
229
|
-
.filter((conversation) => conversation.ownerId === ownerId)
|
|
229
|
+
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
230
230
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
async listSummaries(ownerId
|
|
233
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
234
234
|
this.purgeExpired();
|
|
235
235
|
return Array.from(this.conversations.values())
|
|
236
|
-
.filter((c) => c.ownerId === ownerId)
|
|
236
|
+
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
237
237
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
238
238
|
.map((c) => ({
|
|
239
239
|
conversationId: c.conversationId,
|
|
@@ -454,10 +454,10 @@ class FileConversationStore implements ConversationStore {
|
|
|
454
454
|
await this.writing;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
async list(ownerId
|
|
457
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
458
458
|
await this.ensureLoaded();
|
|
459
459
|
const summaries = Array.from(this.conversations.values())
|
|
460
|
-
.filter((conversation) => conversation.ownerId === ownerId)
|
|
460
|
+
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
461
461
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
462
462
|
const conversations: Conversation[] = [];
|
|
463
463
|
for (const summary of summaries) {
|
|
@@ -469,10 +469,10 @@ class FileConversationStore implements ConversationStore {
|
|
|
469
469
|
return conversations;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
async listSummaries(ownerId
|
|
472
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
473
473
|
await this.ensureLoaded();
|
|
474
474
|
return Array.from(this.conversations.values())
|
|
475
|
-
.filter((c) => c.ownerId === ownerId)
|
|
475
|
+
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
476
476
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
477
477
|
.map((c) => ({
|
|
478
478
|
conversationId: c.conversationId,
|
|
@@ -751,11 +751,15 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
753
|
|
|
754
|
-
async list(ownerId
|
|
754
|
+
async list(ownerId?: string): Promise<Conversation[]> {
|
|
755
755
|
const kv = await this.client();
|
|
756
756
|
if (!kv) {
|
|
757
757
|
return await this.memoryFallback.list(ownerId);
|
|
758
758
|
}
|
|
759
|
+
if (!ownerId) {
|
|
760
|
+
// KV stores index per-owner; cross-owner listing not supported
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
759
763
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
760
764
|
const conversations: Conversation[] = [];
|
|
761
765
|
for (const id of ids) {
|
|
@@ -772,11 +776,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
772
776
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
773
777
|
}
|
|
774
778
|
|
|
775
|
-
async listSummaries(ownerId
|
|
779
|
+
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
776
780
|
const kv = await this.client();
|
|
777
781
|
if (!kv) {
|
|
778
782
|
return await this.memoryFallback.listSummaries(ownerId);
|
|
779
783
|
}
|
|
784
|
+
if (!ownerId) {
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
780
787
|
const ids = await this.getOwnerConversationIds(ownerId);
|
|
781
788
|
const summaries: ConversationSummary[] = [];
|
|
782
789
|
for (const id of ids) {
|
package/test/mcp.test.ts
CHANGED
|
@@ -126,6 +126,89 @@ describe("mcp bridge protocol transports", () => {
|
|
|
126
126
|
expect(deleteSeen).toBe(true);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it("sends custom headers alongside bearer token", async () => {
|
|
130
|
+
process.env.LINEAR_TOKEN = "token-123";
|
|
131
|
+
let capturedHeaders: Record<string, string | undefined> = {};
|
|
132
|
+
const server = createServer(async (req, res) => {
|
|
133
|
+
if (req.method === "DELETE") {
|
|
134
|
+
res.statusCode = 200;
|
|
135
|
+
res.end();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const chunks: Buffer[] = [];
|
|
139
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
140
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
141
|
+
const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
|
|
142
|
+
capturedHeaders = {
|
|
143
|
+
authorization: req.headers.authorization,
|
|
144
|
+
"x-custom-id": req.headers["x-custom-id"] as string | undefined,
|
|
145
|
+
"x-another": req.headers["x-another"] as string | undefined,
|
|
146
|
+
};
|
|
147
|
+
if (payload.method === "initialize") {
|
|
148
|
+
res.setHeader("Content-Type", "application/json");
|
|
149
|
+
res.setHeader("Mcp-Session-Id", "s");
|
|
150
|
+
res.end(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: payload.id,
|
|
154
|
+
result: {
|
|
155
|
+
protocolVersion: "2025-03-26",
|
|
156
|
+
capabilities: { tools: { listChanged: true } },
|
|
157
|
+
serverInfo: { name: "remote", version: "1.0.0" },
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (payload.method === "notifications/initialized") {
|
|
164
|
+
res.statusCode = 202;
|
|
165
|
+
res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (payload.method === "tools/list") {
|
|
169
|
+
res.setHeader("Content-Type", "application/json");
|
|
170
|
+
res.end(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
id: payload.id,
|
|
174
|
+
result: {
|
|
175
|
+
tools: [
|
|
176
|
+
{ name: "ping", inputSchema: { type: "object", properties: {} } },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
res.statusCode = 404;
|
|
184
|
+
res.end();
|
|
185
|
+
});
|
|
186
|
+
await new Promise<void>((r) => server.listen(0, () => r()));
|
|
187
|
+
const address = server.address();
|
|
188
|
+
if (!address || typeof address === "string") throw new Error("Unexpected address");
|
|
189
|
+
|
|
190
|
+
const bridge = new LocalMcpBridge({
|
|
191
|
+
mcp: [
|
|
192
|
+
{
|
|
193
|
+
name: "custom-headers",
|
|
194
|
+
url: `http://127.0.0.1:${address.port}/mcp`,
|
|
195
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
|
|
196
|
+
headers: { "X-Custom-Id": "user-42", "X-Another": "value" },
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await bridge.startLocalServers();
|
|
202
|
+
await bridge.discoverTools();
|
|
203
|
+
|
|
204
|
+
expect(capturedHeaders.authorization).toBe("Bearer token-123");
|
|
205
|
+
expect(capturedHeaders["x-custom-id"]).toBe("user-42");
|
|
206
|
+
expect(capturedHeaders["x-another"]).toBe("value");
|
|
207
|
+
|
|
208
|
+
await bridge.stopLocalServers();
|
|
209
|
+
await new Promise<void>((r) => server.close(() => r()));
|
|
210
|
+
});
|
|
211
|
+
|
|
129
212
|
it("fails fast on duplicate server names", () => {
|
|
130
213
|
expect(
|
|
131
214
|
() =>
|