applaunchflow 0.3.6 → 0.3.8

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/README.md CHANGED
@@ -68,6 +68,16 @@ Public endpoints:
68
68
  - Protected resource metadata: `https://mcp.applaunchflow.com/.well-known/oauth-protected-resource`
69
69
  - Health: `https://mcp.applaunchflow.com/healthz`
70
70
 
71
+ ## Official MCP Registry
72
+
73
+ AppLaunchFlow is published as `io.github.ynnickw/applaunchflow` in the official
74
+ MCP Registry. The checked-in [`server.json`](server.json) is the canonical
75
+ registry manifest and points clients to the hosted OAuth connector.
76
+
77
+ Registry publication runs automatically from GitHub Actions when the manifest
78
+ changes on `main`. Keep the manifest version aligned with `package.json`; the
79
+ test suite enforces this before publication.
80
+
71
81
  `APPLAUNCHFLOW_MCP_PUBLIC_URL` may be either the origin or the full `/mcp`
72
82
  URL; both services normalize it to the same canonical resource URL. Set
73
83
  `APPLAUNCHFLOW_MCP_PUBLIC_URL=https://mcp.applaunchflow.com/mcp` and
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import test from "node:test";
4
+ import { AppLaunchFlowClient } from "./client/api.js";
5
+ import { runWithRequestSignal } from "./request-context.js";
6
+ async function withUnresponsiveServer(callback) {
7
+ const server = createServer(() => undefined);
8
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
9
+ const address = server.address();
10
+ try {
11
+ await callback(`http://127.0.0.1:${address.port}`);
12
+ }
13
+ finally {
14
+ server.closeAllConnections();
15
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
16
+ }
17
+ }
18
+ test("dashboard requests honor explicit timeouts", async () => {
19
+ await withUnresponsiveServer(async (baseUrl) => {
20
+ const client = new AppLaunchFlowClient({ baseUrl, token: "test-token" });
21
+ await assert.rejects(client.requestJson("/slow", { timeoutMs: 20 }), (error) => error instanceof Error && error.name === "TimeoutError");
22
+ });
23
+ });
24
+ test("dashboard requests inherit MCP request cancellation", async () => {
25
+ await withUnresponsiveServer(async (baseUrl) => {
26
+ const client = new AppLaunchFlowClient({ baseUrl, token: "test-token" });
27
+ const controller = new AbortController();
28
+ const request = runWithRequestSignal(controller.signal, () => client.listProjects());
29
+ controller.abort();
30
+ await assert.rejects(request, (error) => error instanceof Error && error.name === "AbortError");
31
+ });
32
+ });
package/build/cli-core.js CHANGED
@@ -15,6 +15,18 @@ export function codexLoginArgs() {
15
15
  export function codexStatusArgs() {
16
16
  return ["mcp", "get", APPLAUNCHFLOW_MCP_NAME];
17
17
  }
18
+ export function codexInspectArgs() {
19
+ return [...codexStatusArgs(), "--json"];
20
+ }
21
+ export function isHostedCodexConfig(value) {
22
+ if (!value || typeof value !== "object")
23
+ return false;
24
+ const transport = value.transport;
25
+ if (!transport || typeof transport !== "object")
26
+ return false;
27
+ const { type, url } = transport;
28
+ return type === "streamable_http" && url === APPLAUNCHFLOW_MCP_URL;
29
+ }
18
30
  export function codexDisconnectArgs() {
19
31
  return ["mcp", "remove", APPLAUNCHFLOW_MCP_NAME];
20
32
  }
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { APPLAUNCHFLOW_MCP_NAME, APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexLoginArgs, codexStatusArgs, } from "./cli-core.js";
3
+ import { APPLAUNCHFLOW_MCP_NAME, APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexInspectArgs, codexLoginArgs, codexStatusArgs, isHostedCodexConfig, } from "./cli-core.js";
4
4
  test("Codex convenience commands use the hosted OAuth connector", () => {
5
5
  assert.deepEqual(codexAddArgs(), [
6
6
  "mcp",
@@ -11,12 +11,40 @@ test("Codex convenience commands use the hosted OAuth connector", () => {
11
11
  ]);
12
12
  assert.deepEqual(codexLoginArgs(), ["mcp", "login", "applaunchflow"]);
13
13
  assert.deepEqual(codexStatusArgs(), ["mcp", "get", "applaunchflow"]);
14
+ assert.deepEqual(codexInspectArgs(), [
15
+ "mcp",
16
+ "get",
17
+ "applaunchflow",
18
+ "--json",
19
+ ]);
14
20
  assert.deepEqual(codexDisconnectArgs(), [
15
21
  "mcp",
16
22
  "remove",
17
23
  "applaunchflow",
18
24
  ]);
19
25
  });
26
+ test("detects whether Codex already uses the hosted connector", () => {
27
+ assert.equal(isHostedCodexConfig({
28
+ transport: {
29
+ type: "streamable_http",
30
+ url: APPLAUNCHFLOW_MCP_URL,
31
+ },
32
+ }), true);
33
+ assert.equal(isHostedCodexConfig({
34
+ transport: {
35
+ type: "stdio",
36
+ command: "npx",
37
+ args: ["-y", "@applaunchflow/mcp@latest"],
38
+ },
39
+ }), false);
40
+ assert.equal(isHostedCodexConfig({
41
+ transport: {
42
+ type: "streamable_http",
43
+ url: "https://example.com/mcp",
44
+ },
45
+ }), false);
46
+ assert.equal(isHostedCodexConfig(null), false);
47
+ });
20
48
  test("the public connector URL is a secure MCP endpoint", () => {
21
49
  const url = new URL(APPLAUNCHFLOW_MCP_URL);
22
50
  assert.equal(url.protocol, "https:");
package/build/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexLoginArgs, codexStatusArgs, } from "./cli-core.js";
3
+ import { APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexInspectArgs, codexLoginArgs, codexStatusArgs, isHostedCodexConfig, } from "./cli-core.js";
4
4
  function printHelp() {
5
5
  console.log(`AppLaunchFlow MCP
6
6
 
@@ -20,16 +20,34 @@ function runCodex(args) {
20
20
  throw new Error(`Codex exited with status ${result.status ?? "unknown"}`);
21
21
  }
22
22
  }
23
- function codexIsConfigured() {
24
- const result = spawnSync("codex", codexStatusArgs(), { stdio: "ignore" });
23
+ function getCodexConfiguration() {
24
+ const result = spawnSync("codex", codexInspectArgs(), {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ });
25
28
  if (result.error) {
26
29
  throw new Error(`Could not run Codex: ${result.error.message}`);
27
30
  }
28
- return result.status === 0;
31
+ if (result.status !== 0)
32
+ return "missing";
33
+ try {
34
+ return isHostedCodexConfig(JSON.parse(result.stdout))
35
+ ? "hosted"
36
+ : "legacy";
37
+ }
38
+ catch {
39
+ return "legacy";
40
+ }
29
41
  }
30
42
  function connectCodex() {
31
- if (!codexIsConfigured()) {
43
+ const configuration = getCodexConfiguration();
44
+ if (configuration === "legacy") {
45
+ console.log("Replacing the legacy local AppLaunchFlow MCP configuration...");
46
+ runCodex(codexDisconnectArgs());
47
+ }
48
+ if (configuration !== "hosted") {
32
49
  runCodex(codexAddArgs());
50
+ return;
33
51
  }
34
52
  runCodex(codexLoginArgs());
35
53
  }
@@ -1,3 +1,7 @@
1
+ import { upstreamSignal } from "../request-context.js";
2
+ const DEFAULT_API_TIMEOUT_MS = 30_000;
3
+ const LONG_RUNNING_API_TIMEOUT_MS = 10 * 60_000;
4
+ const UPLOAD_TIMEOUT_MS = 2 * 60_000;
1
5
  export class AppLaunchFlowApiError extends Error {
2
6
  status;
3
7
  body;
@@ -45,6 +49,7 @@ export class AppLaunchFlowClient {
45
49
  const response = await fetch(url, {
46
50
  method: options.method || "GET",
47
51
  headers,
52
+ signal: upstreamSignal(options.timeoutMs ?? DEFAULT_API_TIMEOUT_MS, options.signal),
48
53
  body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
49
54
  });
50
55
  const contentType = response.headers.get("content-type") || "";
@@ -74,6 +79,7 @@ export class AppLaunchFlowClient {
74
79
  "Content-Type": contentType,
75
80
  },
76
81
  body: new Uint8Array(buffer),
82
+ signal: upstreamSignal(UPLOAD_TIMEOUT_MS),
77
83
  });
78
84
  if (!response.ok) {
79
85
  throw new Error(`Upload failed with status ${response.status}`);
@@ -106,6 +112,7 @@ export class AppLaunchFlowClient {
106
112
  return this.requestJson("/api/screenshots/generate", {
107
113
  method: "POST",
108
114
  body,
115
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
109
116
  });
110
117
  }
111
118
  applyScreenshotTemplate(body) {
@@ -163,6 +170,7 @@ export class AppLaunchFlowClient {
163
170
  return this.requestJson("/api/promovideo/generate", {
164
171
  method: "POST",
165
172
  body,
173
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
166
174
  });
167
175
  }
168
176
  updatePromoVideo(body) {
@@ -214,6 +222,7 @@ export class AppLaunchFlowClient {
214
222
  return this.requestJson("/api/screenshots/translate", {
215
223
  method: "POST",
216
224
  body,
225
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
217
226
  });
218
227
  }
219
228
  listVariants(generationId, contentType) {
@@ -257,6 +266,7 @@ export class AppLaunchFlowClient {
257
266
  return this.requestJson("/api/graphics/generate", {
258
267
  method: "POST",
259
268
  body,
269
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
260
270
  });
261
271
  }
262
272
  applyGraphicsTemplate(body) {
@@ -286,6 +296,7 @@ export class AppLaunchFlowClient {
286
296
  return this.requestJson("/api/aso/copy", {
287
297
  method: "POST",
288
298
  body,
299
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
289
300
  });
290
301
  }
291
302
  updateAsoCopy(body) {
@@ -298,12 +309,14 @@ export class AppLaunchFlowClient {
298
309
  return this.requestJson("/api/aso/translate", {
299
310
  method: "POST",
300
311
  body,
312
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
301
313
  });
302
314
  }
303
315
  suggestCompetitors(body) {
304
316
  return this.requestJson("/api/aso/competitors/suggest", {
305
317
  method: "POST",
306
318
  body,
319
+ timeoutMs: LONG_RUNNING_API_TIMEOUT_MS,
307
320
  });
308
321
  }
309
322
  listSharedIllustrations(query) {
package/build/http.js CHANGED
@@ -3,7 +3,9 @@ import { createServer } from "node:http";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { createAppLaunchFlowServer } from "./index.js";
6
+ import { upstreamSignal } from "./request-context.js";
6
7
  const DEFAULT_PORT = 8787;
8
+ const INTROSPECTION_TIMEOUT_MS = 10_000;
7
9
  const DEFAULT_DASHBOARD_URL = "https://dashboard.applaunchflow.com";
8
10
  const REQUIRED_SCOPES = [
9
11
  "projects:read",
@@ -63,8 +65,16 @@ function unauthorized(request, response) {
63
65
  "www-authenticate": `Bearer resource_metadata="${resourceMetadataUrl(request)}"`,
64
66
  });
65
67
  }
68
+ function methodNotAllowed(response) {
69
+ json(response, 405, {
70
+ jsonrpc: "2.0",
71
+ error: { code: -32000, message: "Method not allowed" },
72
+ id: null,
73
+ }, { allow: "POST" });
74
+ }
66
75
  async function introspectToken(request, token) {
67
76
  const response = await fetch(`${dashboardBaseUrl()}/api/auth/mcp/introspect`, {
77
+ signal: upstreamSignal(INTROSPECTION_TIMEOUT_MS),
68
78
  headers: {
69
79
  authorization: `Bearer ${token}`,
70
80
  accept: "application/json",
@@ -113,6 +123,10 @@ async function handleMcp(request, response) {
113
123
  unauthorized(request, response);
114
124
  return;
115
125
  }
126
+ if (request.method !== "POST") {
127
+ methodNotAllowed(response);
128
+ return;
129
+ }
116
130
  const server = createAppLaunchFlowServer({
117
131
  baseUrl: dashboardBaseUrl(),
118
132
  token,
@@ -184,7 +198,7 @@ async function main() {
184
198
  const port = Number(process.env.PORT || DEFAULT_PORT);
185
199
  const server = createHttpServer();
186
200
  server.listen(port, "0.0.0.0", () => {
187
- console.error(`AppLaunchFlow MCP HTTP server listening on port ${port}`);
201
+ console.log(`AppLaunchFlow MCP HTTP server listening on port ${port}`);
188
202
  });
189
203
  }
190
204
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -116,6 +116,19 @@ test("authenticated Streamable HTTP clients can initialize and discover tools",
116
116
  try {
117
117
  await withServer(async (baseUrl) => {
118
118
  process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL = `${baseUrl}/mcp`;
119
+ for (const method of ["GET", "DELETE"]) {
120
+ const response = await fetch(`${baseUrl}/mcp`, {
121
+ method,
122
+ headers: { authorization: "Bearer test-access-token" },
123
+ });
124
+ assert.equal(response.status, 405);
125
+ assert.equal(response.headers.get("allow"), "POST");
126
+ assert.deepEqual(await response.json(), {
127
+ jsonrpc: "2.0",
128
+ error: { code: -32000, message: "Method not allowed" },
129
+ id: null,
130
+ });
131
+ }
119
132
  const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
120
133
  requestInit: {
121
134
  headers: { authorization: "Bearer test-access-token" },
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { createRequire } from "node:module";
2
3
  import { AppLaunchFlowClient, } from "./client/api.js";
3
4
  import { registerPrompts } from "./prompts/register.js";
4
5
  import { registerResources } from "./resources/register.js";
@@ -14,6 +15,7 @@ import { registerLocalizationTools } from "./tools/localization.js";
14
15
  import { registerVariantTools } from "./tools/variants.js";
15
16
  import { registerKeywordTools } from "./tools/keywords.js";
16
17
  import { installToolMetadataPolicy } from "./tool-metadata.js";
18
+ const packageJson = createRequire(import.meta.url)("../package.json");
17
19
  export const SERVER_INSTRUCTIONS = `
18
20
  AppLaunchFlow MCP supports four content types: app store screenshots, social graphics, promo videos, and mockup animations.
19
21
  Use it for project setup, screenshot uploads, AI generation of screenshots/graphics/videos, mockup animation editing, variant management, direct layout editing, and translation.
@@ -106,7 +108,7 @@ export function createAppLaunchFlowServer(credentials) {
106
108
  const client = new AppLaunchFlowClient(credentials);
107
109
  const server = new McpServer({
108
110
  name: "applaunchflow-mcp",
109
- version: "0.3.3",
111
+ version: packageJson.version,
110
112
  }, {
111
113
  instructions: HOSTED_SERVER_INSTRUCTIONS,
112
114
  });
@@ -0,0 +1,13 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const requestSignalStorage = new AsyncLocalStorage();
3
+ export function runWithRequestSignal(signal, callback) {
4
+ return signal ? requestSignalStorage.run(signal, callback) : callback();
5
+ }
6
+ export function upstreamSignal(timeoutMs, explicitSignal) {
7
+ const signals = [
8
+ explicitSignal,
9
+ requestSignalStorage.getStore(),
10
+ AbortSignal.timeout(timeoutMs),
11
+ ].filter((signal) => signal !== undefined);
12
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
13
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { runWithRequestSignal } from "./request-context.js";
2
3
  const readOnly = {
3
4
  readOnlyHint: true,
4
5
  destructiveHint: false,
@@ -94,6 +95,34 @@ export function installToolMetadataPolicy(server, options = {}) {
94
95
  throw new Error(`Missing tool safety annotations for ${name}`);
95
96
  }
96
97
  const existingMeta = (config._meta || {});
98
+ const toolCallback = callback;
99
+ const instrumentedCallback = async (...args) => {
100
+ const startedAt = performance.now();
101
+ const extra = args[1];
102
+ try {
103
+ const result = await runWithRequestSignal(extra?.signal, () => toolCallback(...args));
104
+ const isError = typeof result === "object" &&
105
+ result !== null &&
106
+ result.isError === true;
107
+ console.log(JSON.stringify({
108
+ event: "mcp_tool",
109
+ tool: name,
110
+ outcome: isError ? "error" : "success",
111
+ durationMs: Math.round(performance.now() - startedAt),
112
+ }));
113
+ return result;
114
+ }
115
+ catch (error) {
116
+ console.error(JSON.stringify({
117
+ event: "mcp_tool",
118
+ tool: name,
119
+ outcome: "exception",
120
+ durationMs: Math.round(performance.now() - startedAt),
121
+ errorType: error instanceof Error ? error.name : "UnknownError",
122
+ }));
123
+ throw error;
124
+ }
125
+ };
97
126
  return registerTool(name, {
98
127
  ...config,
99
128
  annotations: {
@@ -107,6 +136,6 @@ export function installToolMetadataPolicy(server, options = {}) {
107
136
  ? { securitySchemes: [OAUTH_SECURITY_SCHEME] }
108
137
  : {}),
109
138
  },
110
- }, callback);
139
+ }, instrumentedCallback);
111
140
  });
112
141
  }
@@ -1,4 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import { createRequire } from "node:module";
2
4
  import test from "node:test";
3
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
6
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
@@ -24,9 +26,63 @@ test("all registered tools expose submission safety metadata and output schemas"
24
26
  assert.ok(tool.description, `${tool.name} must have a description`);
25
27
  assert.deepEqual(tool._meta?.securitySchemes?.map((scheme) => scheme.type), ["oauth2"]);
26
28
  }
29
+ assert.equal(client.getServerVersion()?.version, createRequire(import.meta.url)("../package.json")
30
+ .version);
27
31
  }
28
32
  finally {
29
33
  await client.close();
30
34
  await server.close();
31
35
  }
32
36
  });
37
+ test("hosted tools emit privacy-safe structured outcome logs", async () => {
38
+ const api = createServer((request, response) => {
39
+ if (request.url === "/api/projects") {
40
+ response.writeHead(200, { "content-type": "application/json" });
41
+ response.end(JSON.stringify({ projects: [] }));
42
+ return;
43
+ }
44
+ if (request.url?.startsWith("/api/app/")) {
45
+ response.writeHead(500, { "content-type": "application/json" });
46
+ response.end(JSON.stringify({ error: "synthetic backend failure" }));
47
+ return;
48
+ }
49
+ response.writeHead(404).end();
50
+ });
51
+ await new Promise((resolve) => api.listen(0, "127.0.0.1", resolve));
52
+ const address = api.address();
53
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
54
+ const server = createAppLaunchFlowServer({
55
+ baseUrl: `http://127.0.0.1:${address.port}`,
56
+ token: "secret-test-token",
57
+ });
58
+ const client = new Client({ name: "logging-test", version: "1.0.0" });
59
+ const logs = [];
60
+ const originalLog = console.log;
61
+ console.log = (...args) => logs.push(args.map(String).join(" "));
62
+ try {
63
+ await server.connect(serverTransport);
64
+ await client.connect(clientTransport);
65
+ const result = await client.callTool({ name: "list_projects", arguments: {} });
66
+ assert.equal(result.isError, undefined);
67
+ const failed = await client.callTool({
68
+ name: "get_project",
69
+ arguments: { projectId: "00000000-0000-4000-8000-000000000001" },
70
+ });
71
+ assert.equal(failed.isError, true);
72
+ assert.equal(logs.length, 2);
73
+ const entries = logs.map((line) => JSON.parse(line));
74
+ assert.deepEqual(entries.map(({ event, tool, outcome }) => ({ event, tool, outcome })), [
75
+ { event: "mcp_tool", tool: "list_projects", outcome: "success" },
76
+ { event: "mcp_tool", tool: "get_project", outcome: "error" },
77
+ ]);
78
+ assert.equal(entries.every((entry) => typeof entry.durationMs === "number"), true);
79
+ assert.equal(logs.some((line) => line.includes("secret-test-token")), false);
80
+ assert.equal(logs.some((line) => line.includes("synthetic backend failure")), false);
81
+ }
82
+ finally {
83
+ console.log = originalLog;
84
+ await client.close();
85
+ await server.close();
86
+ await new Promise((resolve, reject) => api.close((error) => (error ? reject(error) : resolve())));
87
+ }
88
+ });
@@ -3,6 +3,7 @@ import { promises as dns } from "node:dns";
3
3
  import { BlockList, isIP } from "node:net";
4
4
  import path from "path";
5
5
  import { z } from "zod";
6
+ import { upstreamSignal } from "../request-context.js";
6
7
  import { fail, ok } from "./utils.js";
7
8
  const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
8
9
  const MAX_REMOTE_REDIRECTS = 3;
@@ -87,7 +88,7 @@ async function fetchRemoteAsset(value) {
87
88
  for (let redirectCount = 0; redirectCount <= MAX_REMOTE_REDIRECTS; redirectCount += 1) {
88
89
  const response = await fetch(current, {
89
90
  redirect: "manual",
90
- signal: AbortSignal.timeout(20_000),
91
+ signal: upstreamSignal(20_000),
91
92
  headers: { accept: "image/*,font/*;q=0.8" },
92
93
  });
93
94
  if (response.status < 300 || response.status >= 400) {
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { listPublicTemplateIds } from "../catalog.js";
4
4
  import { buildTemplateGalleryUrl } from "../template-previews.js";
5
5
  import { openUrl, fail, ok, startProgressHeartbeat } from "./utils.js";
6
+ import { upstreamSignal } from "../request-context.js";
6
7
  export function registerScreenshotTools(server, client) {
7
8
  server.registerTool("prepare_screenshot_styles", {
8
9
  title: "Prepare Personalized Screenshot Styles",
@@ -294,7 +295,10 @@ export function registerScreenshotTools(server, client) {
294
295
  const previewUrl = `${client.credentials.baseUrl}/api/preview?path=${encodeURIComponent(fullPath)}&w=320`;
295
296
  const headers = new Headers();
296
297
  headers.set("Authorization", `Bearer ${client.credentials.token}`);
297
- const response = await fetch(previewUrl, { headers });
298
+ const response = await fetch(previewUrl, {
299
+ headers,
300
+ signal: upstreamSignal(30_000),
301
+ });
298
302
  if (!response.ok) {
299
303
  throw new Error(`Failed to fetch image: ${response.status}`);
300
304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applaunchflow",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Hosted OAuth MCP connector for AppLaunchFlow.",
5
5
  "license": "MIT",
6
6
  "repository": {