# Project export: Make-It

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: For the plans that actually "Make-It" out of the group chat.
- Devpost: https://devpost.com/software/madeit-a21ey5
- GitHub: https://github.com/ameshabanjara/treehacks26
- Demo: https://youtube.com/shorts/JxpaWyYiINk?feature=share
- Video: https://www.youtube.com/embed/DPMyS21_dpU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 6 GitHub contributor(s) — seantai1 (10 commits), ameshabanjara (7 commits), Efrain Angon (5 commits), alfred (4 commits), Cursor (4 commits), AlfredYu106 (1 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# MCP Server Template

A minimal [FastMCP](https://github.com/jlowin/fastmcp) server template for Render deployment with streamable HTTP transport.

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/InteractionCo/mcp-server-template)

## Local Development

### Setup

Fork the repo, then run:

```bash
git clone <your-repo-url>
cd mcp-server-template
conda create -n mcp-server python=3.13
conda activate mcp-server
pip install -r requirements.txt
```

### Test

```bash
python src/server.py
# then in another terminal run:
npx @modelcontextprotocol/inspector
```

Open http://localhost:3000 and connect to `http://localhost:8000/mcp` using "Streamable HTTP" transport (NOTE THE `/mcp`!).

## Deployment

### Option 1: One-Click Deploy
Click the "Deploy to Render" button above.

### Option 2: Manual Deployment
1. Fork this repository
2. Connect your GitHub account to Render
3. Create a new Web Service on Render
4. Connect your forked repository
5. Render will automatically detect the `render.yaml` configuration

Your server will be available at `https://your-service-name.onrender.com/mcp` (NOTE THE `/mcp`!)

## Poke Setup

You can connect your MCP server to Poke at (poke.com/settings/connections)[poke.com/settings/connections].
To test the connection explitly, ask poke somethink like `Tell the subagent to use the "{connection name}" integration's "{tool name}" tool`.
If you run into persistent issues of poke not calling the right MCP (e.g. after you've renamed the connection) you may send `clearhistory` to poke to delete all message history and start fresh.
We're working hard on improving the integration use of Poke :)


## Customization

Add more tools by decorating functions with `@mcp.tool`:

```python
@mcp.tool
def calculate(x: float, y: float, operation: str) -> float:
    """Perform basic arithmetic operations."""
    if operation == "add":
        return x + y
    elif operation == "multiply":
        return x * y
    # ...
```


## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 94 KB.
- Anthropic (technology) — detected in the code
- Python (language) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository
- AI coding agent: Cursor — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (26 of 26)

```
.gitignore
AGENTS.md
group-chat.ts
modal_app.py
my-stagehand-app/.cursorrules
my-stagehand-app/.env.example
my-stagehand-app/claude.md
my-stagehand-app/index.ts
my-stagehand-app/package.json
my-stagehand-app/README.md
my-stagehand-app/setup-uber-context.ts
my-stagehand-app/tsconfig.json
my-stagehand-app/uber-estimate.ts
package.json
README.md
render.yaml
requirements.txt
send-imessage.ts
src/__tests__/judge.llm.test.ts
src/__tests__/judge.unit.test.ts
src/judge.ts
src/server.py
tsconfig.json
vitest.config.ts
watch-imessage.ts
watcher.ts
```

### Dependencies

- my-stagehand-app/package.json: @browserbasehq/stagehand@latest, dotenv@^16.4.7, tsx@^4.19.2, typescript@^5.0.0
- package.json: @anthropic-ai/sdk@latest, @browserbasehq/stagehand@^3.0.8, @photon-ai/imessage-kit@latest, @types/node@^25.2.3, better-sqlite3@^11.0.0, dotenv@^16.4.7, poke@^0.2.5, tsx@^4.19.2, typescript@^5.0.0, vitest@^3.0.0, zod@^3.23.0
- requirements.txt: fastmcp@>=2.12.0, uvicorn@>=0.35.0

### Recent commits (newest first)

- Merge branch 'main' of github.com:ameshabanjara/treehacks26
- mcp upd
- Merge branch 'main' of https://github.com/ameshabanjara/treehacks26
- updates
- edited context name
- uber mcp logic
- Merge branch 'main' of github.com:ameshabanjara/treehacks26
- update
- Add Modal MCP server with Browserbase context persistence and structured extraction
- added sys prompt
- Merge branch 'main' of github.com:ameshabanjara/treehacks26
- edited prompting stuff
- package change
- env change
- Switch Stagehand to BROWSERBASE for cloud browser (Render compatibility)
- Add make_reservation MCP tool, connect Stagehand booking to server
- Merge branch 'main' of github.com:ameshabanjara/treehacks26
- poke -> gc
- updated
- stream msg from photon to poke

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
Poke - iMessage Planning Agent
Product: Conversational planning agent for iMessage group chats
Version: 1.0
 Last Updated: February 14, 2026

Overview
Poke is an AI agent that lives in iMessage group chats and handles group planning through natural conversation. Add Poke to any group chat, and it will facilitate the entire planning process from gathering preferences to booking reservations.
Core Value: Eliminates the "organizer burden" by having the AI handle logistics, scheduling, and decision-making coordination.

How It Works
Setup
Add Poke to an iMessage group chat (via contact sharing or invite link)
Poke introduces itself and explains how to activate planning mode
Planning Flow
User → "Hi Poke, help us plan for dinner"
  ↓
Poke → "Great! What times are people thinking?"
  ↓
Users → Drop their availability
  • "I'm free after 7pm"
  • "Saturday works for me"
  • "Anytime this weekend!"
  ↓
Poke → "What kind of food are we thinking?"
  ↓
Users → Give suggestions
  • "Italian sounds good"
  • "I'm craving Thai"
  • "Anything but pizza"
  ↓
Poke → Replies with curated list of 3-4 options
  • Rosa's Italian Kitchen - $$, 4.5⭐, available 7pm & 8pm
  • Thai Basil - $$, 4.3⭐, available 6:30pm & 8:30pm
  • Sushi Zen - $$$, 4.7⭐, available 8pm only
  ↓
Users → Majority vote
  • "Rosa's looks great!"
  • "I vote Rosa's"
  • "+1 for Rosa's"
  ↓
Poke → Creates reservation (browser-based booking)
  • Searches for reservation availability
  • Books via OpenTable/Resy/direct site
  ↓
Poke → Summarizes itinerary
  "✅ All set! Here's the plan:
   📍 Rosa's Italian Kitchen
   📅 Saturday, Feb 15
   ⏰ 7:00 PM
   👥 Party of 5
   📝 Reservation #12345
   
   See you there! 🍝"

Agent Capabilities
1. Conversational Planning Intent Detection
Monitors group chat for planning triggers
Activates when explicitly summoned ("Hi Poke...") or detects planning keywords
Maintains context across multi-turn conversations
2. Preference & Constraint Extraction
Extracts from conversation:
Time availability: "after 7pm," "this weekend," "Saturday"
Food preferences: cuisine types, dietary restrictions
Budget signals: "cheap eats," "somewhere nice," "not too expensive"
Location: "near downtown," "walking distance from campus"
Builds per-user preference memory across sessions
3. Multi-Source Venue Search
Uses Browserbase/Stagehand to browse Google, Yelp, and OpenTable directly (no external search APIs needed)
Stagehand's extract() pulls structured data: restaurant name, cuisine, price range, rating, review count, availability
Stagehand's act() navigates search filters: cuisine type, location, party size, date/time
Filters and ranks results by group constraints (cuisine preferences, budget signals, location, time availability)
Returns top 3-4 options optimized for the group
4. Decision Facilitation
Tracks responses and votes
Shows live tally ("3 votes for Rosa's, 1 for Thai Basil")
Prompts non-responders when needed
Declares decision when threshold reached (majority or unanimous)
5.
[truncated — 6433 more characters]
```

### my-stagehand-app/claude.md

```markdown
# Stagehand Project

This is a project that uses Stagehand V3, a browser automation framework with AI-powered `act`, `extract`, `observe`, and `agent` methods.

The main class can be imported as `Stagehand` from `@browserbasehq/stagehand`.

**Key Classes:**

- `Stagehand`: Main orchestrator class providing `act`, `extract`, `observe`, and `agent` methods
- `context`: A `V3Context` object that manages browser contexts and pages
- `page`: Individual page objects accessed via `stagehand.context.pages()[i]` or created with `stagehand.context.newPage()`

## Initialize

```typescript
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "LOCAL", // or "BROWSERBASE"
  verbose: 2, // 0, 1, or 2
  model: "openai/gpt-4.1-mini", // or any supported model
});

await stagehand.init();

// Access the browser context and pages
const page = stagehand.context.pages()[0];
const context = stagehand.context;

// Create new pages if needed
const page2 = await stagehand.context.newPage();
```

## Act

Actions are called on the `stagehand` instance (not the page). Use atomic, specific instructions:

```typescript
// Act on the current active page
await stagehand.act("click the sign in button");

// Act on a specific page (when you need to target a page that isn't currently active)
await stagehand.act("click the sign in button", { page: page2 });
```

**Important:** Act instructions should be atomic and specific:

- ✅ Good: "Click the sign in button" or "Type 'hello' into the search input"
- ❌ Bad: "Order me pizza" or "Type in the search bar and hit enter" (multi-step)

### Observe + Act Pattern (Recommended)

Cache the results of `observe` to avoid unexpected DOM changes:

```typescript
const instruction = "Click the sign in button";

// Get candidate actions
const actions = await stagehand.observe(instruction);

// Execute the first action
await stagehand.act(actions[0]);
```

To target a specific page:

```typescript
const actions = await stagehand.observe("select blue as the favorite color", {
  page: page2,
});
await stagehand.act(actions[0], { page: page2 });
```

## Extract

Extract data from pages using natural language instructions. The `extract` method is called on the `stagehand` instance.

### Basic Extraction (with schema)

```typescript
import { z } from "zod/v3";

// Extract with explicit schema
const data = await stagehand.extract(
  "extract all apartment listings with prices and addresses",
  z.object({
    listings: z.array(
      z.object({
        price: z.string(),
        address: z.string(),
      }),
    ),
  }),
);

console.log(data.listings);
```

### Simple Extraction (without schema)

```typescript
// Extract returns a default object with 'extraction' field
const result = await stagehand.extract("extract the sign in button text");

console.log(result);
// Output: { extraction: "Sign in" }

// Or destructure directly
const { extraction } = await stagehand.extract(
  "extract the sign in button text",

[truncated — 3656 more characters]
```

### requirements.txt

```
fastmcp>=2.12.0
uvicorn>=0.35.0

```

### package.json

```
{
  "name": "treehacks26",
  "version": "1.0.0",
  "type": "module",
  "description": "iMessage group planning agent: Photon + LLM Judge + Poke + BrowserBase",
  "scripts": {
    "start": "tsx src/index.ts",
    "test": "vitest run",
    "test:unit": "vitest run src/__tests__/judge.unit.test.ts",
    "test:llm": "vitest run src/__tests__/judge.llm.test.ts",
    "test:watch": "vitest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@anthropic-ai/sdk": "latest",
    "@browserbasehq/stagehand": "^3.0.8",
    "@photon-ai/imessage-kit": "latest",
    "better-sqlite3": "^11.0.0",
    "dotenv": "^16.4.7",
    "poke": "^0.2.5",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/node": "^25.2.3",
    "tsx": "^4.19.2",
    "typescript": "^5.0.0",
    "vitest": "^3.0.0"
  }
}

```

### my-stagehand-app/package.json

```
{
  "name": "my-stagehand-app",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "tsx index.ts"
  },
  "dependencies": {
    "@browserbasehq/stagehand": "latest",
    "dotenv": "^16.4.7"
  },
  "devDependencies": {
    "tsx": "^4.19.2",
    "typescript": "^5.0.0"
  }
}

```

### my-stagehand-app/index.ts

```typescript
import { Stagehand } from "@browserbasehq/stagehand";

type BookingInput = {
  url: string;
  time_text: string;
  party_size: number;
};

async function readStdin(): Promise<string> {
  return new Promise((resolve) => {
    let data = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (chunk) => (data += chunk));
    process.stdin.on("end", () => resolve(data));
  });
}

async function main() {
  console.info("Launching browser...");
  
  const contextId = process.env.BROWSERBASE_CONTEXT_ID;
  
  const stagehand = new Stagehand({
    env: "BROWSERBASE",
    modelApiKey: process.env.GOOGLE_API_KEY,
    model: "gemini-2.0-flash",
    browserbaseSessionCreateParams: {
      browserSettings: {
        context: { id: contextId, persist: true },
      },
    },
  });

  await stagehand.init();

  const page = stagehand.context.pages()[0];

  console.info("Connected!");

  const stdin = await readStdin();
  if (!stdin.trim()) {
    console.error(
      "No JSON provided. Pipe JSON with url, time_text, party_size."
    );
    await stagehand.close();
    process.exit(1);
  }

  const { url, time_text, party_size } = JSON.parse(stdin) as BookingInput;

  await new Promise((resolve) => setTimeout(resolve, 1000));

  await page.goto(url);
  await page.waitForLoadState("networkidle");
  await stagehand.act(`select party size ${party_size}`);
  await stagehand.act("click the view full availability button");
  await stagehand.act(`click the ${time_text} button`);
  await stagehand.act("click the phone number input box");
  await stagehand.act("click the complete reservation button");

  // Wait 3 seconds for page to load before extracting
  await new Promise((resolve) => setTimeout(resolve, 3000));

  // Extract confirmation and output JSON for MCP server
  let confirmation: Record<string, unknown> | null = null;
  try {
    const extracted = await stagehand.extract(
      "Extract reservation details: restaurant name, confirmation number, date, time, party size, address"
    );
    confirmation = typeof extracted === "object" && extracted ? (extracted as Record<string, unknown>) : null;
  } catch {
    // ignore
  }

  const result = {
    success: true,
    confirmation,
    url,
    time: time_text,
    party_size,
  };
  console.log(JSON.stringify(result));

  console.info("Success!");

  console.info("Waiting 30 seconds before closing...");
  await new Promise((resolve) => setTimeout(resolve, 30000));

  await stagehand.close();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

```

### src/server.py

```python
#!/usr/bin/env python3
import os
import re
import json
import uuid
import time
import urllib.parse
from dataclasses import dataclass, asdict
from typing import List, Dict, Optional, Any, Tuple

from fastmcp import FastMCP

mcp = FastMCP("Group Planner MCP")

# -----------------------------
# Config / constants
# -----------------------------

FINAL_TAG = "[FINAL_TO_SEND]"
RESPONSE_TAG = "[GROUP_PLANNER_RESPONSE]"
DEFAULT_TZ = os.environ.get("DEFAULT_TZ", "America/Los_Angeles")  # informational only
BOOKING_WEBHOOK_URL = os.environ.get("BOOKING_WEBHOOK_URL")  # optional: teammate booking service
BOOKING_WEBHOOK_SECRET = os.environ.get("BOOKING_WEBHOOK_SECRET")  # optional auth header/token

# If you don't want persistence, leave as in-memory.
# For hackathon demo this is fine (single instance). If you scale, use Redis.
PLANS: Dict[str, Dict[str, Any]] = {}
BOOKING_JOBS: Dict[str, Dict[str, Any]] = {}

# -----------------------------
# Helpers
# -----------------------------

def _now_ts() -> float:
    return time.time()

def _new_id(prefix: str) -> str:
    return f"{prefix}_{uuid.uuid4().hex[:10]}"

def _safe_list(x: Any) -> List:
    if x is None:
        return []
    return x if isinstance(x, list) else [x]

def _dedup(items: List[str]) -> List[str]:
    seen = set()
    out = []
    for it in items:
        it2 = (it or "").strip()
        if not it2:
            continue
        if it2.lower() in seen:
            continue
        seen.add(it2.lower())
        out.append(it2)
    return out

def _extract_money(text: str) -> Optional[str]:
    # Rough: "$35", "under 40", "max 30"
    m = re.search(r"\$\s?(\d{1,3})", text)
    if m:
        return f"${m.group(1)}"
    m = re.search(r"(under|max|budget)\s+(\d{1,3})", text)
    if m:
        return f"<=${m.group(2)}"
    return None

def _extract_party_size(text: str) -> Optional[int]:
    # Rough: "we have 5", "party of 6", "6 ppl"
    m = re.search(r"(party of|we have|we're|we are|for)\s+(\d{1,2})", text)
    if m:
        try:
            return int(m.group(2))
        except Exception:
            return None
    m = re.search(r"(\d{1,2})\s*(people|ppl|friends|of us)", text)
    if m:
        try:
            return int(m.group(1))
        except Exception:
            return None
    return None

def _extract_time_window(text: str) -> Optional[str]:
    # Rough: "after 6:30", "7ish", "7:30", "between 7 and 8"
    # Return a human string; you can formalize later.
    t = text.lower()
    # Between X and Y
    m = re.search(r"between\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s+and\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?", t)
    if m:
        a_h, a_m, a_ap = m.group(1), m.group(2) or "00", m.group(3) or ""
        b_h, b_m, b_ap = m.group(4), m.group(5) or "00", m.group(6) or ""
        return f"between {a_h}:{a_m}{a_ap} and {b_h}:{b_m}{b_ap}".strip()

    # After X
    m = re.search(r"after\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?", t)
    if m:
        hh, mm, ap = m.group(1), m.group(2) or "00", m.group(3) or ""
        return f"after {hh}:{mm}{ap}".strip()

    # Exact time
    m = re.search(r"\b(\d{1,2})(?::(\d{2}))\s*(am|pm)\b", t)
    if m:
        return f"around {m.group(1)}:{m.group(2)}{m.group(3)}"
    m = re.search(r"\b(\d{1,2})\s*(am|pm)\b", t)
    if m:
        return f"around {m.group(1)}{m.group(2)}"

    # "7ish"
    m = re.search(r"\b(\d{1,2})\s*ish\b", t)
    if m:
        return f"around {m.group(1)}"

    return None

def _extract_date_hint(text: str) -> Optional[str]:
    t = text.lower()
    # Very rough: friday/sat/sunday/tomorrow
    for d in ["today", "tomorrow", "friday", "saturday", "sunday", "monday", "tuesday", "wednesday", "thursday"]:
        if d in t:
            return d.capitalize()
    # "Feb 20"
    m = re.search(r"\b(jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\s+(\d{1,2})\b", t)
    if m:
        return f"{m.group(1).capitalize()} {m.group(2)}"
    return None

def _extract_location(text: str) -> Optional[str]:
    t = text.lower()
    # Add your own hotspots
    if "palo alto" in t:
        return "Palo Alto"
    if "stanford" in t or "campus" in t:
        return "Near Stanford / campus"
    if "downtown" in t and "palo alto" in t:
        return "Downtown Palo Alto"
    if "sf" in t or "san francisco" in t:
        return "San Francisco"
    if "mission" in t:
        return "Mission (SF)"
    if "san jose" in t or "sj" in t:
        return "San Jose"
    return None

def _extract_dietary(text: str) -> Tuple[List[str], List[str]]:
    t = text.lower()
    dietary = []
    avoid = []

    # dietary flags
    if "vegetarian" in t or "veggie" in t:
        dietary.append("vegetarian")
    if "vegan" in t:
        dietary.append("vegan")
    if "gluten" in t:
        dietary.append("gluten-free")
    if "halal" in t:
        dietary.append("halal")
    if "kosher" in t:
        dietary.append("kosher")

    # avoid / allergies
    if "no shellfish" in t or "shellfish allergy" in t:
        avoid.append("shellfish")
    if "allergy" in t and "peanut" in t:
        avoid.append("peanuts")
    if "no sushi" in t:
        avoid.append("sushi")

    return _dedup(dietary), _dedup(avoid)

def _extract_vibe(text: str) -> List[str]:
    t = text.lower()
    vibe = []
    for key, tag in [
        ("cute", "cute"),
        ("cozy", "cozy"),
        ("chill", "chill"),
        ("lively", "lively"),
        ("quiet", "quiet"),
        ("not loud", "not too loud"),
        ("mid energy", "mid energy"),
        ("casual", "casual"),
        ("fancy", "fancy"),
        ("date night", "not date-night"),
        ("fun", "fun"),
    ]:
        if key in t:
            vibe.append(tag)
    return _dedup(vibe)

def _merge_constraints(base: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:
    out = dict(base or {})
    for k, v in (new or {}).items():
        if v is None:
            continue
        if isinstance(v, list):
            out[k] = _dedup(_safe_list(out.get(k)) + v)
        else:
            
[truncated — 26594 more characters]
```

### vitest.config.ts

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: false,
    environment: "node",
    include: ["src/__tests__/**/*.test.ts"],
  },
});

```

### send-imessage.ts

```typescript
import { IMessageSDK } from '@photon-ai/imessage-kit'

const sdk = new IMessageSDK()

// Replace with the phone number or email you want to message
await sdk.send('+14083689761', 'Hello from iMessage Kit!')

await sdk.close()

```

### render.yaml

```yaml
services:
  - type: web
    name: fastmcp-server
    runtime: python
    buildCommand: pip install -r requirements.txt
    startCommand: python src/server.py
    plan: free
    autoDeploy: false
    envVars:
      - key: ENVIRONMENT
        value: production

```

[9 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]