patchwork-os 0.2.0-alpha.3 → 0.2.0-alpha.5
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/dist/bridge.js +23 -10
- package/dist/bridge.js.map +1 -1
- package/dist/connectors/github.d.ts +58 -8
- package/dist/connectors/github.js +321 -84
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gmail.js +7 -0
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +57 -0
- package/dist/connectors/googleCalendar.js +308 -0
- package/dist/connectors/googleCalendar.js.map +1 -0
- package/dist/connectors/linear.d.ts +52 -19
- package/dist/connectors/linear.js +167 -129
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpClient.d.ts +56 -0
- package/dist/connectors/mcpClient.js +189 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/connectors/mcpOAuth.d.ts +73 -0
- package/dist/connectors/mcpOAuth.js +338 -0
- package/dist/connectors/mcpOAuth.js.map +1 -0
- package/dist/connectors/sentry.d.ts +17 -21
- package/dist/connectors/sentry.js +124 -131
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/recipes/yamlRunner.js +32 -42
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +13 -1
- package/dist/recipesHttp.js +9 -1
- package/dist/recipesHttp.js.map +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +220 -49
- package/dist/server.js.map +1 -1
- package/dist/tools/createLinearIssue.d.ts +84 -0
- package/dist/tools/createLinearIssue.js +146 -0
- package/dist/tools/createLinearIssue.js.map +1 -0
- package/dist/tools/fetchCalendarEvents.d.ts +94 -0
- package/dist/tools/fetchCalendarEvents.js +97 -0
- package/dist/tools/fetchCalendarEvents.js.map +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +80 -0
- package/dist/tools/fetchGithubIssue.js +84 -0
- package/dist/tools/fetchGithubIssue.js.map +1 -0
- package/dist/tools/fetchGithubPR.d.ts +89 -0
- package/dist/tools/fetchGithubPR.js +96 -0
- package/dist/tools/fetchGithubPR.js.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/start-all.sh +56 -19
- package/templates/recipes/ctx-loop-test.yaml +75 -0
- package/templates/recipes/morning-brief.yaml +12 -4
- package/templates/recipes/sentry-to-linear.yaml +77 -0
|
@@ -1,167 +1,184 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Linear connector.
|
|
2
|
+
* Linear connector — routes through Linear's official MCP server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Env var: LINEAR_API_KEY
|
|
4
|
+
* Endpoint: https://mcp.linear.app/mcp
|
|
5
|
+
* Auth: OAuth 2.1 w/ PKCE; dynamic client registration (RFC 7591).
|
|
7
6
|
*
|
|
8
|
-
* HTTP routes
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* HTTP routes (wired in src/server.ts):
|
|
8
|
+
* GET /connections/linear/authorize — returns { url } for popup
|
|
9
|
+
* GET /connections/linear/callback — token exchange
|
|
10
|
+
* POST /connections/linear/test — ping MCP server
|
|
11
|
+
* DELETE /connections/linear — revoke + delete token
|
|
12
|
+
*
|
|
13
|
+
* Back-compat: loadTokens() returns a shape compatible with legacy code
|
|
14
|
+
* that expected { api_key }. Set LINEAR_API_KEY to bypass OAuth for CI/headless.
|
|
12
15
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
import { McpClient } from "./mcpClient.js";
|
|
17
|
+
import { completeAuthorize, getAccessToken, loadTokenFile, revoke, startAuthorize, vendorConfig, } from "./mcpOAuth.js";
|
|
18
|
+
const LINEAR_MCP_ENDPOINT = "https://mcp.linear.app/mcp";
|
|
19
|
+
// ── MCP client ───────────────────────────────────────────────────────────────
|
|
20
|
+
let _client = null;
|
|
21
|
+
function client() {
|
|
22
|
+
if (!_client) {
|
|
23
|
+
_client = new McpClient(LINEAR_MCP_ENDPOINT, async () => {
|
|
24
|
+
const envKey = process.env.LINEAR_API_KEY;
|
|
25
|
+
if (envKey)
|
|
26
|
+
return envKey;
|
|
27
|
+
return getAccessToken("linear");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return _client;
|
|
31
|
+
}
|
|
32
|
+
// ── Back-compat token loader ─────────────────────────────────────────────────
|
|
19
33
|
export function loadTokens() {
|
|
20
34
|
const envKey = process.env.LINEAR_API_KEY;
|
|
21
35
|
if (envKey) {
|
|
22
36
|
return { api_key: envKey, connected_at: new Date().toISOString() };
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(readFileSync(TOKEN_PATH, "utf-8"));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
38
|
+
const file = loadTokenFile("linear");
|
|
39
|
+
if (!file)
|
|
30
40
|
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function saveTokens(tokens) {
|
|
34
|
-
mkdirSync(path.dirname(TOKEN_PATH), { recursive: true, mode: 0o700 });
|
|
35
|
-
writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
36
|
-
}
|
|
37
|
-
function deleteTokens() {
|
|
38
|
-
if (existsSync(TOKEN_PATH))
|
|
39
|
-
unlinkSync(TOKEN_PATH);
|
|
40
|
-
}
|
|
41
|
-
export function getStatus() {
|
|
42
|
-
const tokens = loadTokens();
|
|
43
41
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
workspace: tokens?.workspace,
|
|
42
|
+
api_key: file.access_token,
|
|
43
|
+
workspace: file.profile?.workspace,
|
|
44
|
+
connected_at: file.connected_at,
|
|
48
45
|
};
|
|
49
46
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
headers: {
|
|
55
|
-
"Content-Type": "application/json",
|
|
56
|
-
Authorization: apiKey,
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify({ query, variables }),
|
|
59
|
-
signal,
|
|
60
|
-
});
|
|
61
|
-
if (!res.ok) {
|
|
62
|
-
const body = await res.text();
|
|
63
|
-
throw new Error(`Linear API error ${res.status}: ${body.slice(0, 200)}`);
|
|
64
|
-
}
|
|
65
|
-
const json = (await res.json());
|
|
66
|
-
if (json.errors?.length) {
|
|
67
|
-
throw new Error(`Linear GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
47
|
+
export function getStatus() {
|
|
48
|
+
const envKey = process.env.LINEAR_API_KEY;
|
|
49
|
+
if (envKey) {
|
|
50
|
+
return { id: "linear", status: "connected" };
|
|
68
51
|
}
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const VIEWER_QUERY = `query { viewer { id name email organization { name urlKey } } }`;
|
|
72
|
-
async function verifyToken(apiKey, signal) {
|
|
73
|
-
const data = await linearQuery(VIEWER_QUERY, {}, apiKey, signal);
|
|
52
|
+
const file = loadTokenFile("linear");
|
|
74
53
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
54
|
+
id: "linear",
|
|
55
|
+
status: file ? "connected" : "disconnected",
|
|
56
|
+
lastSync: file?.connected_at,
|
|
57
|
+
workspace: file?.profile?.workspace,
|
|
78
58
|
};
|
|
79
59
|
}
|
|
80
|
-
const ISSUE_QUERY = `
|
|
81
|
-
query GetIssue($id: String!) {
|
|
82
|
-
issue(id: $id) {
|
|
83
|
-
id
|
|
84
|
-
identifier
|
|
85
|
-
title
|
|
86
|
-
description
|
|
87
|
-
state { name type }
|
|
88
|
-
assignee { name email }
|
|
89
|
-
priority
|
|
90
|
-
priorityLabel
|
|
91
|
-
url
|
|
92
|
-
createdAt
|
|
93
|
-
updatedAt
|
|
94
|
-
team { name key }
|
|
95
|
-
labels { nodes { name } }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
`;
|
|
99
|
-
/**
|
|
100
|
-
* Fetch a Linear issue by ID or URL.
|
|
101
|
-
* Accepts: "LIN-123", "abc123def456...", "https://linear.app/.../issue/LIN-123/..."
|
|
102
|
-
*/
|
|
103
|
-
export async function fetchIssue(issueIdOrUrl, signal) {
|
|
104
|
-
const tokens = loadTokens();
|
|
105
|
-
if (!tokens) {
|
|
106
|
-
throw new Error("Linear not connected. POST /connections/linear/connect first.");
|
|
107
|
-
}
|
|
108
|
-
const id = extractIssueId(issueIdOrUrl);
|
|
109
|
-
const data = await linearQuery(ISSUE_QUERY, { id }, tokens.api_key, signal);
|
|
110
|
-
if (!data.issue) {
|
|
111
|
-
throw new Error(`Linear issue not found: ${id}`);
|
|
112
|
-
}
|
|
113
|
-
return data.issue;
|
|
114
|
-
}
|
|
115
60
|
function extractIssueId(issueIdOrUrl) {
|
|
116
|
-
// URL form: https://linear.app/org/issue/LIN-123/title
|
|
117
61
|
const urlMatch = issueIdOrUrl.match(/\/issue\/([A-Z]+-\d+|[a-f0-9-]{36})/i);
|
|
118
62
|
if (urlMatch)
|
|
119
63
|
return urlMatch[1];
|
|
120
|
-
|
|
121
|
-
if (/^[A-Z]+-\d+$/i.test(
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return issueIdOrUrl.trim();
|
|
64
|
+
const trimmed = issueIdOrUrl.trim();
|
|
65
|
+
if (/^[A-Z]+-\d+$/i.test(trimmed))
|
|
66
|
+
return trimmed;
|
|
67
|
+
if (/^[a-f0-9-]{36}$/i.test(trimmed))
|
|
68
|
+
return trimmed;
|
|
126
69
|
throw new Error(`Cannot parse Linear issue ID from: ${issueIdOrUrl}`);
|
|
127
70
|
}
|
|
128
|
-
export async function
|
|
129
|
-
|
|
130
|
-
|
|
71
|
+
export async function fetchIssue(issueIdOrUrl, signal) {
|
|
72
|
+
if (!loadTokens())
|
|
73
|
+
throw new Error("Linear not connected. GET /connections/linear/authorize first.");
|
|
74
|
+
const id = extractIssueId(issueIdOrUrl);
|
|
75
|
+
const res = await client().callTool("get_issue", { id }, { signal });
|
|
76
|
+
const parsed = McpClient.extractJson(res);
|
|
77
|
+
const issue = parsed.issue ?? parsed;
|
|
78
|
+
if (!issue)
|
|
79
|
+
throw new Error(`Linear issue not found: ${id}`);
|
|
80
|
+
return issue;
|
|
81
|
+
}
|
|
82
|
+
export async function listIssues(opts = {}, signal) {
|
|
83
|
+
if (!loadTokens())
|
|
84
|
+
return [];
|
|
85
|
+
const args = {
|
|
86
|
+
limit: Math.min(opts.limit ?? 20, 50),
|
|
87
|
+
};
|
|
88
|
+
if (opts.team)
|
|
89
|
+
args.team = opts.team;
|
|
90
|
+
if (opts.assigneeMe)
|
|
91
|
+
args.assignee = "me";
|
|
92
|
+
if (opts.states?.length)
|
|
93
|
+
args.stateTypes = opts.states;
|
|
94
|
+
try {
|
|
95
|
+
const res = await client().callTool("list_issues", args, {
|
|
96
|
+
signal,
|
|
97
|
+
cacheKey: `linear:issues:${JSON.stringify(args)}`,
|
|
98
|
+
cacheTtlMs: 60_000,
|
|
99
|
+
});
|
|
100
|
+
const parsed = McpClient.extractJson(res);
|
|
101
|
+
if (Array.isArray(parsed))
|
|
102
|
+
return parsed;
|
|
103
|
+
return parsed.issues ?? parsed.nodes ?? [];
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof Error && err.message.includes("not connected"))
|
|
107
|
+
throw err;
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── HTTP handlers ────────────────────────────────────────────────────────────
|
|
112
|
+
export async function handleLinearAuthorize() {
|
|
113
|
+
try {
|
|
114
|
+
const { url } = await startAuthorize(vendorConfig("linear"));
|
|
115
|
+
return { status: 302, body: "", redirect: url };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
131
118
|
return {
|
|
132
119
|
status: 400,
|
|
133
120
|
contentType: "application/json",
|
|
134
|
-
body: JSON.stringify({
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
ok: false,
|
|
123
|
+
error: err instanceof Error ? err.message : String(err),
|
|
124
|
+
}),
|
|
135
125
|
};
|
|
136
126
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
}
|
|
128
|
+
export async function handleLinearCallback(code, state, error) {
|
|
129
|
+
if (error) {
|
|
130
|
+
return {
|
|
131
|
+
status: 400,
|
|
132
|
+
contentType: "text/html",
|
|
133
|
+
body: `<html><body><h2>Linear connect failed</h2><pre>${error}</pre></body></html>`,
|
|
143
134
|
};
|
|
144
|
-
|
|
135
|
+
}
|
|
136
|
+
if (!code || !state) {
|
|
137
|
+
return {
|
|
138
|
+
status: 400,
|
|
139
|
+
contentType: "text/html",
|
|
140
|
+
body: `<html><body><h2>Linear connect failed</h2><pre>missing code/state</pre></body></html>`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await completeAuthorize(vendorConfig("linear"), code, state);
|
|
145
|
+
// Best-effort profile capture (workspace name)
|
|
146
|
+
try {
|
|
147
|
+
const res = await client().callTool("get_viewer", {}, { timeoutMs: 10_000 });
|
|
148
|
+
const viewer = McpClient.extractJson(res);
|
|
149
|
+
const workspace = viewer.organization?.urlKey ?? viewer.organization?.name ?? "";
|
|
150
|
+
if (workspace) {
|
|
151
|
+
const file = loadTokenFile("linear");
|
|
152
|
+
if (file) {
|
|
153
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
154
|
+
const { homedir } = await import("node:os");
|
|
155
|
+
const path = await import("node:path");
|
|
156
|
+
const p = path.join(homedir(), ".patchwork", "tokens", "linear-mcp.json");
|
|
157
|
+
mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
158
|
+
file.profile = { ...(file.profile ?? {}), workspace };
|
|
159
|
+
writeFileSync(p, JSON.stringify(file, null, 2), { mode: 0o600 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Profile fetch is best-effort
|
|
165
|
+
}
|
|
145
166
|
return {
|
|
146
167
|
status: 200,
|
|
147
|
-
contentType: "
|
|
148
|
-
body:
|
|
168
|
+
contentType: "text/html",
|
|
169
|
+
body: `<html><body><h2>Linear connected</h2><script>window.close();</script></body></html>`,
|
|
149
170
|
};
|
|
150
171
|
}
|
|
151
172
|
catch (err) {
|
|
152
173
|
return {
|
|
153
174
|
status: 400,
|
|
154
|
-
contentType: "
|
|
155
|
-
body:
|
|
156
|
-
ok: false,
|
|
157
|
-
error: err instanceof Error ? err.message : String(err),
|
|
158
|
-
}),
|
|
175
|
+
contentType: "text/html",
|
|
176
|
+
body: `<html><body><h2>Linear connect failed</h2><pre>${err instanceof Error ? err.message : String(err)}</pre></body></html>`,
|
|
159
177
|
};
|
|
160
178
|
}
|
|
161
179
|
}
|
|
162
180
|
export async function handleLinearTest() {
|
|
163
|
-
|
|
164
|
-
if (!tokens) {
|
|
181
|
+
if (!loadTokens()) {
|
|
165
182
|
return {
|
|
166
183
|
status: 400,
|
|
167
184
|
contentType: "application/json",
|
|
@@ -169,11 +186,11 @@ export async function handleLinearTest() {
|
|
|
169
186
|
};
|
|
170
187
|
}
|
|
171
188
|
try {
|
|
172
|
-
const
|
|
189
|
+
const ok = await client().ping({ timeoutMs: 10_000 });
|
|
173
190
|
return {
|
|
174
|
-
status: 200,
|
|
191
|
+
status: ok ? 200 : 400,
|
|
175
192
|
contentType: "application/json",
|
|
176
|
-
body: JSON.stringify({ ok:
|
|
193
|
+
body: JSON.stringify({ ok, message: ok ? "connected" : "ping failed" }),
|
|
177
194
|
};
|
|
178
195
|
}
|
|
179
196
|
catch (err) {
|
|
@@ -187,12 +204,33 @@ export async function handleLinearTest() {
|
|
|
187
204
|
};
|
|
188
205
|
}
|
|
189
206
|
}
|
|
190
|
-
export function handleLinearDisconnect() {
|
|
191
|
-
|
|
207
|
+
export async function handleLinearDisconnect() {
|
|
208
|
+
await revoke("linear");
|
|
209
|
+
_client = null;
|
|
192
210
|
return {
|
|
193
211
|
status: 200,
|
|
194
212
|
contentType: "application/json",
|
|
195
213
|
body: JSON.stringify({ ok: true }),
|
|
196
214
|
};
|
|
197
215
|
}
|
|
216
|
+
export async function listTeams(signal) {
|
|
217
|
+
const res = await client().callTool("list_teams", {}, { signal });
|
|
218
|
+
const parsed = McpClient.extractJson(res);
|
|
219
|
+
if (Array.isArray(parsed))
|
|
220
|
+
return parsed;
|
|
221
|
+
return parsed.teams ?? parsed.nodes ?? [];
|
|
222
|
+
}
|
|
223
|
+
export async function listLabels(signal) {
|
|
224
|
+
const res = await client().callTool("list_issue_labels", {}, { signal });
|
|
225
|
+
const parsed = McpClient.extractJson(res);
|
|
226
|
+
if (Array.isArray(parsed))
|
|
227
|
+
return parsed;
|
|
228
|
+
return parsed.labels ?? parsed.nodes ?? [];
|
|
229
|
+
}
|
|
230
|
+
export async function createIssue(input, signal) {
|
|
231
|
+
const res = await client().callTool("save_issue", input, { signal });
|
|
232
|
+
const parsed = McpClient.extractJson(res);
|
|
233
|
+
const issue = parsed.issue ?? parsed;
|
|
234
|
+
return issue;
|
|
235
|
+
}
|
|
198
236
|
//# sourceMappingURL=linear.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear.js","sourceRoot":"","sources":["../../src/connectors/linear.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"linear.js","sourceRoot":"","sources":["../../src/connectors/linear.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Streamable-HTTP MCP client for calling upstream MCP servers
|
|
3
|
+
* (GitHub, Linear, Sentry) from Patchwork connectors.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - initialize handshake
|
|
7
|
+
* - tools/list
|
|
8
|
+
* - tools/call (with argument object)
|
|
9
|
+
* - SSE response parsing (when server returns text/event-stream)
|
|
10
|
+
* - Bearer token auth (OAuth access token)
|
|
11
|
+
* - 15s default timeout
|
|
12
|
+
* - Best-effort 60s in-memory result cache (opt-in per call)
|
|
13
|
+
*
|
|
14
|
+
* Wire format reference: MCP spec 2024-11-05 / 2025-03-26, Streamable HTTP transport.
|
|
15
|
+
*/
|
|
16
|
+
type JsonValue = string | number | boolean | null | {
|
|
17
|
+
[k: string]: JsonValue;
|
|
18
|
+
} | JsonValue[];
|
|
19
|
+
export interface McpCallOptions {
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** If set, cache the result under this key for `cacheTtlMs` ms. */
|
|
23
|
+
cacheKey?: string;
|
|
24
|
+
cacheTtlMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface McpToolResult {
|
|
27
|
+
content: Array<{
|
|
28
|
+
type: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
}>;
|
|
32
|
+
isError?: boolean;
|
|
33
|
+
structuredContent?: JsonValue;
|
|
34
|
+
}
|
|
35
|
+
export declare function clearMcpCache(): void;
|
|
36
|
+
export declare class McpClient {
|
|
37
|
+
private readonly endpoint;
|
|
38
|
+
private readonly getAccessToken;
|
|
39
|
+
private sessionId;
|
|
40
|
+
private initialized;
|
|
41
|
+
private nextId;
|
|
42
|
+
constructor(endpoint: string, getAccessToken: () => Promise<string>);
|
|
43
|
+
private post;
|
|
44
|
+
private ensureInitialized;
|
|
45
|
+
listTools(opts?: McpCallOptions): Promise<Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
inputSchema?: JsonValue;
|
|
49
|
+
}>>;
|
|
50
|
+
callTool(name: string, args: Record<string, unknown>, opts?: McpCallOptions): Promise<McpToolResult>;
|
|
51
|
+
/** Convenience: extract the first `structuredContent` object, or parse the first text block as JSON. */
|
|
52
|
+
static extractJson<T = unknown>(result: McpToolResult): T;
|
|
53
|
+
/** Ping by listing tools; returns true if reachable + authorized. */
|
|
54
|
+
ping(opts?: McpCallOptions): Promise<boolean>;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Streamable-HTTP MCP client for calling upstream MCP servers
|
|
3
|
+
* (GitHub, Linear, Sentry) from Patchwork connectors.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - initialize handshake
|
|
7
|
+
* - tools/list
|
|
8
|
+
* - tools/call (with argument object)
|
|
9
|
+
* - SSE response parsing (when server returns text/event-stream)
|
|
10
|
+
* - Bearer token auth (OAuth access token)
|
|
11
|
+
* - 15s default timeout
|
|
12
|
+
* - Best-effort 60s in-memory result cache (opt-in per call)
|
|
13
|
+
*
|
|
14
|
+
* Wire format reference: MCP spec 2024-11-05 / 2025-03-26, Streamable HTTP transport.
|
|
15
|
+
*/
|
|
16
|
+
const cache = new Map();
|
|
17
|
+
const DEFAULT_TIMEOUT = 15_000;
|
|
18
|
+
function getCached(key) {
|
|
19
|
+
const e = cache.get(key);
|
|
20
|
+
if (!e)
|
|
21
|
+
return null;
|
|
22
|
+
if (Date.now() > e.expiresAt) {
|
|
23
|
+
cache.delete(key);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return e.value;
|
|
27
|
+
}
|
|
28
|
+
function setCached(key, value, ttlMs) {
|
|
29
|
+
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
30
|
+
}
|
|
31
|
+
export function clearMcpCache() {
|
|
32
|
+
cache.clear();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Streamable-HTTP response body. The server may reply with:
|
|
36
|
+
* - application/json → single JSON-RPC response
|
|
37
|
+
* - text/event-stream → SSE frames, last `data:` line is the response
|
|
38
|
+
*/
|
|
39
|
+
async function parseMcpResponse(res) {
|
|
40
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
if (ct.includes("text/event-stream")) {
|
|
43
|
+
// Find last `data:` line carrying a JSON payload
|
|
44
|
+
const lines = text.split(/\r?\n/);
|
|
45
|
+
let last = null;
|
|
46
|
+
for (const l of lines) {
|
|
47
|
+
if (l.startsWith("data:")) {
|
|
48
|
+
const payload = l.slice(5).trim();
|
|
49
|
+
if (payload && payload !== "[DONE]")
|
|
50
|
+
last = payload;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!last)
|
|
54
|
+
throw new Error("MCP SSE response had no data frame");
|
|
55
|
+
return JSON.parse(last);
|
|
56
|
+
}
|
|
57
|
+
if (!text)
|
|
58
|
+
return null;
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
function withTimeout(signal, ms) {
|
|
62
|
+
const ctl = new AbortController();
|
|
63
|
+
const onAbort = () => ctl.abort(signal?.reason);
|
|
64
|
+
if (signal) {
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
ctl.abort(signal.reason);
|
|
67
|
+
else
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
}
|
|
70
|
+
const t = setTimeout(() => ctl.abort(new Error("MCP request timeout")), ms);
|
|
71
|
+
ctl.signal.addEventListener("abort", () => clearTimeout(t), { once: true });
|
|
72
|
+
return ctl.signal;
|
|
73
|
+
}
|
|
74
|
+
export class McpClient {
|
|
75
|
+
endpoint;
|
|
76
|
+
getAccessToken;
|
|
77
|
+
sessionId = null;
|
|
78
|
+
initialized = false;
|
|
79
|
+
nextId = 1;
|
|
80
|
+
constructor(endpoint, getAccessToken) {
|
|
81
|
+
this.endpoint = endpoint;
|
|
82
|
+
this.getAccessToken = getAccessToken;
|
|
83
|
+
}
|
|
84
|
+
async post(body, opts = {}) {
|
|
85
|
+
const token = await this.getAccessToken();
|
|
86
|
+
const signal = withTimeout(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT);
|
|
87
|
+
const headers = {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Accept: "application/json, text/event-stream",
|
|
90
|
+
Authorization: `Bearer ${token}`,
|
|
91
|
+
};
|
|
92
|
+
if (this.sessionId)
|
|
93
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
94
|
+
const res = await fetch(this.endpoint, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
signal,
|
|
99
|
+
});
|
|
100
|
+
// Pick up session id if server issued one
|
|
101
|
+
const sid = res.headers.get("mcp-session-id");
|
|
102
|
+
if (sid)
|
|
103
|
+
this.sessionId = sid;
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
const snippet = (await res.text()).slice(0, 300);
|
|
106
|
+
throw new Error(`MCP HTTP ${res.status} at ${this.endpoint}: ${snippet}`);
|
|
107
|
+
}
|
|
108
|
+
return parseMcpResponse(res);
|
|
109
|
+
}
|
|
110
|
+
async ensureInitialized(opts = {}) {
|
|
111
|
+
if (this.initialized)
|
|
112
|
+
return;
|
|
113
|
+
const id = this.nextId++;
|
|
114
|
+
const resp = (await this.post({
|
|
115
|
+
jsonrpc: "2.0",
|
|
116
|
+
id,
|
|
117
|
+
method: "initialize",
|
|
118
|
+
params: {
|
|
119
|
+
protocolVersion: "2025-03-26",
|
|
120
|
+
capabilities: {},
|
|
121
|
+
clientInfo: { name: "patchwork-os", version: "0.1" },
|
|
122
|
+
},
|
|
123
|
+
}, opts));
|
|
124
|
+
if (resp?.error)
|
|
125
|
+
throw new Error(`MCP initialize failed: ${resp.error.message}`);
|
|
126
|
+
// Notify initialized (fire-and-forget, no id)
|
|
127
|
+
await this.post({ jsonrpc: "2.0", method: "notifications/initialized" }, opts).catch(() => { });
|
|
128
|
+
this.initialized = true;
|
|
129
|
+
}
|
|
130
|
+
async listTools(opts = {}) {
|
|
131
|
+
await this.ensureInitialized(opts);
|
|
132
|
+
const id = this.nextId++;
|
|
133
|
+
const resp = (await this.post({ jsonrpc: "2.0", id, method: "tools/list" }, opts));
|
|
134
|
+
if (resp?.error)
|
|
135
|
+
throw new Error(`tools/list: ${resp.error.message}`);
|
|
136
|
+
return resp.result?.tools ?? [];
|
|
137
|
+
}
|
|
138
|
+
async callTool(name, args, opts = {}) {
|
|
139
|
+
if (opts.cacheKey) {
|
|
140
|
+
const hit = getCached(opts.cacheKey);
|
|
141
|
+
if (hit)
|
|
142
|
+
return hit;
|
|
143
|
+
}
|
|
144
|
+
await this.ensureInitialized(opts);
|
|
145
|
+
const id = this.nextId++;
|
|
146
|
+
const resp = (await this.post({
|
|
147
|
+
jsonrpc: "2.0",
|
|
148
|
+
id,
|
|
149
|
+
method: "tools/call",
|
|
150
|
+
params: { name, arguments: args },
|
|
151
|
+
}, opts));
|
|
152
|
+
if (resp?.error)
|
|
153
|
+
throw new Error(`tools/call ${name}: ${resp.error.message}`);
|
|
154
|
+
const result = resp.result ?? { content: [] };
|
|
155
|
+
if (result.isError) {
|
|
156
|
+
const msg = result.content
|
|
157
|
+
.map((c) => c.text)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(" ")
|
|
160
|
+
.slice(0, 300);
|
|
161
|
+
throw new Error(`MCP tool ${name} returned error: ${msg || "unknown"}`);
|
|
162
|
+
}
|
|
163
|
+
if (opts.cacheKey && opts.cacheTtlMs) {
|
|
164
|
+
setCached(opts.cacheKey, result, opts.cacheTtlMs);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/** Convenience: extract the first `structuredContent` object, or parse the first text block as JSON. */
|
|
169
|
+
static extractJson(result) {
|
|
170
|
+
if (result.structuredContent !== undefined) {
|
|
171
|
+
return result.structuredContent;
|
|
172
|
+
}
|
|
173
|
+
const text = result.content.find((c) => c.type === "text")?.text;
|
|
174
|
+
if (!text)
|
|
175
|
+
throw new Error("MCP result had no text content");
|
|
176
|
+
return JSON.parse(text);
|
|
177
|
+
}
|
|
178
|
+
/** Ping by listing tools; returns true if reachable + authorized. */
|
|
179
|
+
async ping(opts = {}) {
|
|
180
|
+
try {
|
|
181
|
+
await this.listTools(opts);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=mcpClient.js.map
|