Skip to content

feat(deco-flights): flight research MCP agent#3106

Open
vibegui wants to merge 1 commit intomainfrom
vibegui/deco-flights
Open

feat(deco-flights): flight research MCP agent#3106
vibegui wants to merge 1 commit intomainfrom
vibegui/deco-flights

Conversation

@vibegui
Copy link
Copy Markdown
Contributor

@vibegui vibegui commented Apr 13, 2026

CleanShot 2026-04-12 at 23 17 41 CleanShot 2026-04-12 at 23 16 50

Summary

  • New packages/deco-flights/ standalone MCP server for flight research via Google Flights
  • Uses fast-flights-ts@4.1.0 (TypeScript port of fast-flights) for scraping
  • 9 MCP tools: search, trip CRUD, background execution, add searches, stop
  • MCP-app UIs with live dashboard, sortable results table, Google Flights links
  • Background worker with 3x parallel search execution and real-time progress
  • REST API (/api/trips) for iframe polling (bypasses AppBridge limitations)
  • Recruit modal on home screen ("Flights" agent template)
  • Tiered search strategy via agent instructions (small focused trips, drill down with TRIP_ADD_SEARCHES)
  • Open-jaw/multi-city, airline filtering, weighted scoring, currency support

Test plan

  • bun run --cwd packages/deco-flights dev starts server on :4747
  • Install Flights agent from home screen
  • Create a trip via chat, execute research, verify live progress in dashboard
  • Verify Google Flights links work (copy to clipboard)
  • Test TRIP_ADD_SEARCHES for drill-down workflow
  • Test TRIP_STOP cancellation
  • Verify currency displays correctly (USD default)

🤖 Generated with Claude Code


Summary by cubic

Add @decocms/deco-flights, a standalone MCP agent for flight research powered by Google Flights, plus a home-screen Flights template and live dashboards. It runs parallel searches and streams real-time results with deep links to Google Flights.

  • New Features

    • 9 MCP tools: FLIGHT_SEARCH, TRIP_CREATE/GET/LIST/UPDATE/DELETE, TRIP_EXECUTE/STOP, TRIP_ADD_SEARCHES
    • Background worker with 3 concurrent searches, live progress, and cancel support
    • MCP-app UIs: trips dashboard, trip planner, and search results (sortable, with Google Flights links); REST /api/trips for polling
    • Tiered search strategy, open‑jaw/multi‑city, airline include/exclude filters, weighted scoring, and currency support
    • Home screen “Flights” template and recruit modal; added to WELL_KNOWN_AGENT_TEMPLATES
  • Migration

    • Start the server: bun run --cwd packages/deco-flights dev (port 4747)
    • From Home, open the Flights template to install the agent, create a trip, and run research
    • Trip data persists at ~/.deco/flights/trips/

Written for commit b4def5e. Summary will update on new commits.

New standalone MCP package for flight research with Google Flights:

- 9 tools: FLIGHT_SEARCH, TRIP_CREATE/GET/LIST/UPDATE/DELETE, TRIP_EXECUTE/STOP, TRIP_ADD_SEARCHES
- Background worker with parallel search execution (3 concurrent)
- Local JSON persistence at ~/.deco/flights/trips/
- MCP-app UIs: trips dashboard with drill-down, live search progress, sortable results table
- REST API at /api/trips for real-time UI polling
- Tiered search strategy via agent instructions (5-8 searches, then drill down)
- Open-jaw/multi-city support via fast-flights-ts
- Airline inclusion filtering at Google level, exclusion filtering post-fetch
- Google Flights URL per search task (copy to clipboard)
- Weighted result scoring (price, stops, layover, airports, duration)
- Recruit modal on home screen for one-click agent install

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Copy Markdown
Contributor

Release Options

Suggested: Minor (2.260.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.259.1-alpha.1
🎉 Patch 2.259.1
❤️ Minor 2.260.0
🚀 Major 3.0.0

Current version: 2.259.0

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 issues found across 29 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/deco-flights/server/tools/trip-update.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-update.ts:58">
P2: Merging updates should preserve existing `preferences` fields; direct replacement drops previously stored preference values on partial updates.</violation>

<violation number="2" location="packages/deco-flights/server/tools/trip-update.ts:83">
P1: When regenerating the search plan, clear `searchTasks` too; otherwise previously generated tasks are reused and execution can run outdated routes/dates.</violation>
</file>

<file name="packages/deco-flights/server/lib/planner.ts">

<violation number="1" location="packages/deco-flights/server/lib/planner.ts:80">
P2: Sampling with `Math.ceil(totalCombinations / MAX_SEARCHES)` can return far fewer than `MAX_SEARCHES` searches, reducing flight coverage when combinations are capped.</violation>
</file>

<file name="packages/deco-flights/server/lib/storage.ts">

<violation number="1" location="packages/deco-flights/server/lib/storage.ts:13">
P1: Trip IDs are used in file paths without validation, enabling path traversal outside the trips directory.</violation>

<violation number="2" location="packages/deco-flights/server/lib/storage.ts:35">
P2: A single malformed trip JSON file will crash trip listing because JSON parsing errors are not handled.</violation>
</file>

<file name="packages/deco-flights/server/ui/trips-dashboard.ts">

<violation number="1" location="packages/deco-flights/server/ui/trips-dashboard.ts:136">
P1: Unescaped dynamic content is inserted into `innerHTML`, enabling XSS if trip/result fields contain HTML.</violation>
</file>

<file name="packages/deco-flights/server/ui/trip-card.ts">

<violation number="1" location="packages/deco-flights/server/ui/trip-card.ts:98">
P1: Unescaped trip fields are written via `innerHTML`, allowing script/HTML injection in the trip card UI.</violation>
</file>

<file name="packages/deco-flights/server/ui/trip-planner.ts">

<violation number="1" location="packages/deco-flights/server/ui/trip-planner.ts:102">
P2: Task sort fallback uses `||`, which converts `running` priority `0` to `3` and breaks the intended status ordering.</violation>

<violation number="2" location="packages/deco-flights/server/ui/trip-planner.ts:135">
P1: Unescaped trip/result data is injected into `innerHTML`, enabling XSS if any field contains HTML/JS payloads.</violation>
</file>

<file name="packages/deco-flights/server/lib/worker.ts">

<violation number="1" location="packages/deco-flights/server/lib/worker.ts:141">
P1: Errored tasks are retried indefinitely with no retry limit, which can keep the worker running forever on persistent failures.</violation>

<violation number="2" location="packages/deco-flights/server/lib/worker.ts:146">
P2: The pre-wave save does not actually persist `running` task states, so live progress can be stale during execution.</violation>

<violation number="3" location="packages/deco-flights/server/lib/worker.ts:173">
P1: Final trip status ignores errored/cancelled tasks and can incorrectly mark a cancelled run as `complete`.</violation>
</file>

<file name="packages/deco-flights/server/ui/search-results.ts">

<violation number="1" location="packages/deco-flights/server/ui/search-results.ts:95">
P1: Unsanitized tool-result fields are injected into `innerHTML`, which can allow script/markup injection in the UI.</violation>
</file>

<file name="packages/deco-flights/server/tools/trip-list.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-list.ts:9">
P3: The tool description says results are capped at 10, but the code actually returns up to 20.</violation>
</file>

<file name="packages/deco-flights/server/tools/trip-delete.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-delete.ts:16">
P1: TRIP_DELETE does not stop an active worker, so a running research job can recreate the trip file after deletion.</violation>
</file>

<file name="packages/deco-flights/server/index.ts">

<violation number="1" location="packages/deco-flights/server/index.ts:137">
P1: Avoid wildcard CORS for trip and MCP endpoints; it allows any origin to read potentially sensitive trip data.</violation>
</file>

<file name="packages/deco-flights/server/tools/flight-search.ts">

<violation number="1" location="packages/deco-flights/server/tools/flight-search.ts:81">
P2: Response field name mismatches UI expectations (`googleFlightsUrl` vs `fallbackUrl`), so the fallback link is not shown on empty/error results.</violation>
</file>

<file name="packages/deco-flights/server/tools/trip-stop.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-stop.ts:22">
P1: `TRIP_STOP` performs immediate abort cancellation, not "finish current search then stop" as documented.</violation>

<violation number="2" location="packages/deco-flights/server/tools/trip-stop.ts:33">
P2: `TRIP_STOP` unconditionally sets status to `draft`, which can incorrectly revert completed trips.</violation>
</file>

<file name="packages/deco-flights/server/tools/trip-add-searches.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-add-searches.ts:85">
P1: Adding searches during an active worker run can be overwritten by the worker’s stale snapshot, causing newly added tasks to be lost or never executed.</violation>
</file>

<file name="packages/deco-flights/server/lib/scraper.ts">

<violation number="1" location="packages/deco-flights/server/lib/scraper.ts:98">
P2: Per-leg airline data is overwritten with the first itinerary airline, which breaks avoid/preferred airline filtering for mixed-airline itineraries.</violation>

<violation number="2" location="packages/deco-flights/server/lib/scraper.ts:113">
P1: Stop count is overestimated for multi-leg searches (e.g., round-trips), which can incorrectly filter and down-rank valid itineraries.</violation>
</file>

<file name="packages/deco-flights/server/tools/trip-create.ts">

<violation number="1" location="packages/deco-flights/server/tools/trip-create.ts:41">
P2: Validate `tripLengthDays` bounds (positive integers and `min <= max`) to prevent invalid search plans.</violation>

<violation number="2" location="packages/deco-flights/server/tools/trip-create.ts:45">
P1: Restrict `passengers` to a positive integer; current schema allows invalid counts like 0, negatives, and decimals.</violation>
</file>

<file name="packages/deco-flights/server/lib/agent-instructions.ts">

<violation number="1" location="packages/deco-flights/server/lib/agent-instructions.ts:5">
P2: This makes 10 sound like a hard trip-wide cap, but drill-down searches can still be added after the initial plan.</violation>
</file>

<file name="packages/mesh-sdk/src/lib/constants.ts">

<violation number="1" location="packages/mesh-sdk/src/lib/constants.ts:343">
P2: Add an `appId` for the Flights registry template so the registry lookup can resolve its full metadata.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


if (planChanged) {
trip.searchPlan = generateSearchPlan(trip);
trip.results = undefined;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When regenerating the search plan, clear searchTasks too; otherwise previously generated tasks are reused and execution can run outdated routes/dates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/tools/trip-update.ts, line 83:

<comment>When regenerating the search plan, clear `searchTasks` too; otherwise previously generated tasks are reused and execution can run outdated routes/dates.</comment>

<file context>
@@ -0,0 +1,97 @@
+
+    if (planChanged) {
+      trip.searchPlan = generateSearchPlan(trip);
+      trip.results = undefined;
+      trip.status = "draft";
+    }
</file context>
Fix with Cubic

}

function tripPath(id: string): string {
return join(TRIPS_DIR, `${id}.json`);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Trip IDs are used in file paths without validation, enabling path traversal outside the trips directory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/lib/storage.ts, line 13:

<comment>Trip IDs are used in file paths without validation, enabling path traversal outside the trips directory.</comment>

<file context>
@@ -0,0 +1,96 @@
+}
+
+function tripPath(id: string): string {
+  return join(TRIPS_DIR, `${id}.json`);
+}
+
</file context>
Suggested change
return join(TRIPS_DIR, `${id}.json`);
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
throw new Error(`Invalid trip id: ${id}`);
}
return join(TRIPS_DIR, `${id}.json`);
Fix with Cubic

var created=t.createdAt?new Date(t.createdAt).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'';

h+='<tr style="cursor:pointer" onclick="openTrip(\\''+t.id+'\\')">';
h+='<td class="font-medium">'+t.name+'</td>';
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unescaped dynamic content is inserted into innerHTML, enabling XSS if trip/result fields contain HTML.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/ui/trips-dashboard.ts, line 136:

<comment>Unescaped dynamic content is inserted into `innerHTML`, enabling XSS if trip/result fields contain HTML.</comment>

<file context>
@@ -0,0 +1,354 @@
+      var created=t.createdAt?new Date(t.createdAt).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'';
+
+      h+='<tr style="cursor:pointer" onclick="openTrip(\\''+t.id+'\\')">';
+      h+='<td class="font-medium">'+t.name+'</td>';
+      h+='<td class="text-sm">'+t.origin+' → '+t.destinations.join(', ')+(t.returnOrigins&&t.returnOrigins.length?' <span class="muted">ret '+t.returnOrigins.join(', ')+'</span>':'')+'</td>';
+      h+='<td class="text-sm">'+fmtD(t.earliestDeparture)+' – '+fmtD(t.latestReturn)+'</td>';
</file context>
Fix with Cubic

html += '<div class="mt-2 text-sm muted">' + trip.searchPlan.searches.length + ' searches planned</div>';
}

root.innerHTML = html;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unescaped trip fields are written via innerHTML, allowing script/HTML injection in the trip card UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/ui/trip-card.ts, line 98:

<comment>Unescaped trip fields are written via `innerHTML`, allowing script/HTML injection in the trip card UI.</comment>

<file context>
@@ -0,0 +1,102 @@
+    html += '<div class="mt-2 text-sm muted">' + trip.searchPlan.searches.length + ' searches planned</div>';
+  }
+
+  root.innerHTML = html;
+  resizeHeight(Math.min(root.scrollHeight + 32, 280));
+}
</file context>
Fix with Cubic

var root = document.getElementById('root');
var results = trip.results || [];

var h = '<div class="trip-banner"><h1>'+trip.name+'</h1>';
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unescaped trip/result data is injected into innerHTML, enabling XSS if any field contains HTML/JS payloads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/ui/trip-planner.ts, line 135:

<comment>Unescaped trip/result data is injected into `innerHTML`, enabling XSS if any field contains HTML/JS payloads.</comment>

<file context>
@@ -0,0 +1,243 @@
+  var root = document.getElementById('root');
+  var results = trip.results || [];
+
+  var h = '<div class="trip-banner"><h1>'+trip.name+'</h1>';
+  h += '<div class="route"><span class="code">'+trip.origin+'</span><span style="opacity:0.5">→</span><span class="code">'+trip.destinations.join(', ')+'</span>';
+  h += '<span class="badge badge-'+trip.status+'" style="margin-left:12px">'+trip.status+'</span></div>';
</file context>
Fix with Cubic

const results: FlightResult[] = rawResults.map((r) => ({
price: r.price,
currency: opts.currency || "USD",
flights: r.flights.map((leg) => ({
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-leg airline data is overwritten with the first itinerary airline, which breaks avoid/preferred airline filtering for mixed-airline itineraries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/lib/scraper.ts, line 98:

<comment>Per-leg airline data is overwritten with the first itinerary airline, which breaks avoid/preferred airline filtering for mixed-airline itineraries.</comment>

<file context>
@@ -0,0 +1,140 @@
+    const results: FlightResult[] = rawResults.map((r) => ({
+      price: r.price,
+      currency: opts.currency || "USD",
+      flights: r.flights.map((leg) => ({
+        airline: r.airlines[0] || "Unknown",
+        flightNumber: "",
</file context>
Fix with Cubic

latestReturn: z
.string()
.describe("Latest acceptable return date (YYYY-MM-DD)"),
tripLengthDays: z.object({
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Validate tripLengthDays bounds (positive integers and min <= max) to prevent invalid search plans.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/tools/trip-create.ts, line 41:

<comment>Validate `tripLengthDays` bounds (positive integers and `min <= max`) to prevent invalid search plans.</comment>

<file context>
@@ -0,0 +1,97 @@
+    latestReturn: z
+      .string()
+      .describe("Latest acceptable return date (YYYY-MM-DD)"),
+    tripLengthDays: z.object({
+      min: z.number().describe("Minimum trip length in days"),
+      max: z.number().describe("Maximum trip length in days"),
</file context>
Fix with Cubic


## CRITICAL RULES

1. **MAX 10 SEARCHES PER TRIP.** The system hard-caps at 10. Keep date ranges tight.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This makes 10 sound like a hard trip-wide cap, but drill-down searches can still be added after the initial plan.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/lib/agent-instructions.ts, line 5:

<comment>This makes 10 sound like a hard trip-wide cap, but drill-down searches can still be added after the initial plan.</comment>

<file context>
@@ -0,0 +1,54 @@
+
+## CRITICAL RULES
+
+1. **MAX 10 SEARCHES PER TRIP.** The system hard-caps at 10. Keep date ranges tight.
+2. **ALWAYS set currency in preferences.** Default to USD. Ask the user if unclear.
+   Example: \`preferences: { currency: "USD", maxStops: 1 }\`
</file context>
Fix with Cubic

type: "pack" as const,
},
{
id: "deco-flights",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add an appId for the Flights registry template so the registry lookup can resolve its full metadata.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-sdk/src/lib/constants.ts, line 343:

<comment>Add an `appId` for the Flights registry template so the registry lookup can resolve its full metadata.</comment>

<file context>
@@ -333,6 +339,12 @@ export const WELL_KNOWN_AGENT_TEMPLATES = [
     type: "pack" as const,
   },
+  {
+    id: "deco-flights",
+    title: "Flights",
+    icon: "icon://Compass03?color=sky",
</file context>
Suggested change
id: "deco-flights",
id: "deco-flights",
appId: "deco/flights",
Fix with Cubic

export const TRIP_LIST = createTool({
id: "TRIP_LIST",
description:
"List all saved trips with search tasks and top results (capped at 10 per trip).",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The tool description says results are capped at 10, but the code actually returns up to 20.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deco-flights/server/tools/trip-list.ts, line 9:

<comment>The tool description says results are capped at 10, but the code actually returns up to 20.</comment>

<file context>
@@ -0,0 +1,34 @@
+export const TRIP_LIST = createTool({
+  id: "TRIP_LIST",
+  description:
+    "List all saved trips with search tasks and top results (capped at 10 per trip).",
+  annotations: {
+    title: "List Trips",
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant