npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cursor/july

v0.1.18

Published

(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.

Readme

@cursor/july

[!WARNING] Early alpha. @cursor/july is the codename release of Cursor's agent framework (the CLI is agentkit). It is under active development: APIs, the CLI surface, and the package name itself will change before a stable release, and 0.x versions may ship breaking changes without notice.

A filesystem-first framework for building and serving Cursor agents. Customers define an agent as ordinary files — markdown for prose, TypeScript for typed behavior — under an agent/ directory. The framework discovers those files, compiles them into a manifest, and serves the agent over channels, using the Cursor SDK (@cursor/sdk) and the Cursor harness as the execution engine.

User-facing documentation lives in docs/ — also served at /docs by every running agent-serve serve host — getting started, concepts, guides (Slack, GitHub webhooks, approvals, agent-to-agent, cloud runtime), evals, live A/B metrics, hillclimbing, deployment, troubleshooting, and reference pages for each folder. This README is the compact package reference.

Instead of one large configuration object, each part of the agent gets a clear home. Instructions go in one file, tools in one folder, channels in another; a file's location says what it does, and its path gives it its name. There is no registry to keep in sync: add the file and it is discovered, move or rename it and its identity moves with it.

my-agent/
├── package.json
└── agent/
    ├── agent.ts             # runtime config: the Cursor model
    ├── instructions.md      # the always-on system prompt
    ├── tools/
    │   └── get_weather.ts   # one typed tool per file
    ├── skills/
    │   └── forecast.md      # on-demand procedures (SKILL.md convention)
    ├── mcp-connections/
    │   └── linear.ts        # tools from external MCP servers
    ├── subagents/
    │   └── researcher/      # specialist child agents
    ├── channels/
    │   └── webhook.ts       # HTTP surfaces beyond the built-in session API
    ├── hooks/
    │   └── audit.ts         # observe the runtime event stream
    ├── ab.ts                # optional live A/B experiment
    ├── ab/                  # optional: more experiments
    ├── schedules/
    │   └── heartbeat.md     # cron-driven runs
    ├── sandbox/workspace/   # files seeded into each session's workspace
    └── lib/                 # shared code (import-only, never discovered)

Scaffold a starter project with agent-serve init ./my-agent — a minimal agent (package.json, tsconfig.json, agent/agent.ts, agent/instructions.md, and a demo agent/tools/echo.ts) that serves immediately and type-checks with npm run check. From there, grow it folder by folder; the docs walk a weather agent from a single get_weather tool through skills, channels, evals, and deployment.

Serve one project with agent-serve serve --dir ./my-agent --dev, or point serve at a folder of agent projects to mount every child under its directory name.

Node only — do not run under Bun

Run agent-serve with Node 22+ (from source: pnpm exec tsx src/bin/agent-serve.ts …, or the built dist/bin/agent-serve.js). Do not run it under Bun: Bun's HTTP/2 client corrupts the Cursor SDK's local harness tool-result streams (NGHTTP2_FRAME_SIZE_ERROR), so every built-in read/grep the model makes fails and turns degrade into failed-read retry loops (we measured an 8-minute review that takes ~1 minute under Node). The mise tasks in this package already use tsx.

Serving many agents at once

Point serve at a folder of agent projects and it hosts all of them on one port, each under its own slug (its directory name):

agent-serve serve --dir ./agents --dev
# 2 agents listening
#   playground  http://127.0.0.1:5273

serve always uses multi-agent layout by default: agents are mounted under /<slug>/v1/* with an index at /. A directory that is itself an agent project is mounted under its directory name. Opt into the unslugged /v1/* surface with serve(dir, { mode: "single" }).

In multi-agent mode:

  • GET / is a web index listing every agent, linking to its playground; GET /v1/agents is the JSON equivalent.
  • GET /v1/health is host-level liveness (for ALB/ECS); each agent also has /<slug>/v1/health.
  • each agent is fully namespaced: /<slug>/v1/session, /<slug>/v1/session/:id/stream, /<slug>/playground, /<slug>/v1/info, custom channels at /<slug>/v1/channels/<id>, etc.
  • sessions are isolated per agent (<stateRoot>/<slug>/), and the playground gains a "← all agents" link back to the index.
  • point the terminal client at a slug: agent-serve chat --url http://127.0.0.1:3000/weather-agent.

Slugs come from directory names and must match [A-Za-z0-9][A-Za-z0-9_-]* and not collide with the reserved v1 / playground / docs path segments.

Agent-to-agent: every agent is an MCP server

Every mounted agent also serves the Model Context Protocol over streamable HTTP at /<slug>/v1/mcp (or /v1/mcp in single mode), so other agents — and any MCP client — can delegate work to it. The surface is stateless (session identity travels in tool arguments) and runs the same route auth chain as the session API. Tools:

| Tool | Behavior | | ---- | -------- | | ask | Send a message; runs a model turn in this agent's own session, tools, and context and returns { status, sessionId, reply }. Omit sessionId for a fresh session; pass it to follow up. | | check | Wait for / poll a running session (waitSeconds: 0 for a non-blocking poll). | | call_tool | Call one of the agent's deterministic server tools directly (no model turn). Registered only when the agent has server tools. |

Waits are bounded (~50s, below MCP client request timeouts): a long turn returns status: "running" and the caller keeps waiting with check. Sessions created this way live on the mcp channel, bind to the calling principal, and show up in the playground and GET /v1/sessions like any other session.

Peer MCP connections make delegation first-class between agents on the same host. Author an MCP connection whose transport is a peer slug:

// agents/concierge/agent/mcp-connections/weather.ts
import { defineConnection } from "@cursor/july/connections";

export default defineConnection({
  agent: "weather-agent",
  description: "Delegate weather questions to the weather agent.",
});

The parent model then sees the peer's ask / check (/ call_tool) tools under the weather server name — subagent-style delegation where the peer keeps its own instructions, tools, MCP connections, and sessions. Peer URLs resolve when the server starts (so run / eval ephemeral ports work):

  • Local-runtime turns (and host-side ctx.host.mcp / channel handlers) call the peer over loopback — works out of the box under the default localDevStrict() auth.
  • Cloud-runtime turns execute on a cloud VM that cannot reach this host's loopback address. Pass --public-url https://agent-serve.example.com (or serve(dir, { publicUrl })) so peers resolve to a reachable URL; without it, peers are omitted from cloud turns (the server warns at startup). With --bearer-token, the token is attached to peer calls automatically so they pass the target agent's auth chain.

Unknown peer slugs and self-references fail at serve startup. Peers require the multi-agent layout (each agent mounted under its slug). There is no cross-host loop protection yet: if agent A's instructions delegate to B and B's delegate back to A, they can recurse — scope each agent's delegation instructions narrowly (the concierge above delegates weather questions to weather-agent, not everything).

agent-serve serve --dir ./agents --dev
agent-serve chat --url http://127.0.0.1:3000/concierge \
  --message "What's the weather in Paris right now?"
# concierge → weather.ask → weather-agent's own session/tools → reply

Quick start

A minimal agent is two files.

agent/instructions.md:

You are a concise assistant. Use tools when they are available.

agent/agent.ts:

import { defineAgent } from "@cursor/july";

export default defineAgent({
  // optional — defaults to grok-4.5 with effort=high and fast=true
  // runtime: "local", // default — Cursor SDK local harness
  // runtime: "cloud",
  // cloud: {
  //   repos: [{ url: "https://github.com/org/repo", startingRef: "main" }],
  // },
});

Serve it (turns run on the Cursor harness, so the host needs a Cursor credential — sign in once, or export an API key):

agent-serve login          # browser sign-in; mints + stores a revocable API key
# or: export CURSOR_API_KEY=key_...
agent-serve serve --dir . --port 3000
# e.g. playground at http://127.0.0.1:3000/<dirname>/playground

Start a session, then follow the NDJSON event stream (replace <slug> with the agent directory name):

curl -X POST http://127.0.0.1:3000/<slug>/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"What can you do?"}'
# {"ok":true,"sessionId":"ses_...","continuationToken":"http:..."}

curl -N http://127.0.0.1:3000/<slug>/v1/session/ses_.../stream
# {"type":"session.started",...}
# {"type":"message.appended","data":{"delta":"I can","text":"I can"},...}
# {"type":"message.completed",...}
# {"type":"turn.completed",...}
# {"type":"session.waiting",...}

Send a follow-up to the same durable session with the continuation token from the previous response (each accepted follow-up rotates it):

curl -X POST http://127.0.0.1:3000/<slug>/v1/session/ses_... \
  -H 'content-type: application/json' \
  -d '{"continuationToken":"http:...","message":"Shorter, please."}'

Inspect the discovered surface at any time:

agent-serve info --dir .        # or GET /<slug>/v1/info on a running server
agent-serve validate --dir .    # exit non-zero on error diagnostics

Terminal client

agent-serve chat talks to a running server over the same public API and renders the reply live — streamed text, tool calls, and a per-turn usage footer. Pass --json for a compact trajectory (same shape as run).

# interactive REPL against a local server
agent-serve chat --url http://127.0.0.1:3000

# one-shot (good for piping / scripts)
agent-serve chat --url http://127.0.0.1:3000 --message "Weather in Paris?"

# JSON trajectory (for coding agents / scripts)
agent-serve chat --url http://127.0.0.1:3000 --message "hello" --json

# against an authed server
agent-serve chat --url https://my-agent.example.com --bearer-token "$TOKEN"

Agent loop

Coding agents should prefer the JSON-first commands: edit files → inspect → run a turn → assert with filesystem evals.

agent-serve validate --dir .
agent-serve info --dir . --json

# Ephemeral server + one (or more) turns; JSON trajectory on stdout
agent-serve run --dir . --message "What's the temperature in NYC?"

# Optional event log for later summarization
agent-serve run --dir . --message "..." --events /tmp/run.ndjson
agent-serve trajectory --events /tmp/run.ndjson

# Scaffold (or print the setup guide with no directory)
agent-serve init ./my-agent
agent-serve init

run / eval boot a temporary server on port 0 with a temp stateRoot outside the project so session workspaces do not inherit ambient monorepo AGENTS.md / .cursor rules. Pass --url to target an already-running agent instead.

Evals

Author cases under evals/**/*.eval.ts with defineEval. Drive the agent and assert inline:

import { defineEval, includes } from "@cursor/july/evals";

export default defineEval({
  description: "Uses get_weather for temperature questions.",
  tags: ["smoke"],
  async test(t) {
    await t.send("What's the temperature in NYC?");
    t.succeeded();
    t.calledTool("get_weather");
    t.check(t.reply, includes(/°|[FC]/));
  },
});

Case id is the path under evals/ for a single-test file, or <fileId>/<case.id> when using cases in one file.

Every project with evals needs evals/evals.config.ts with a required maxConcurrency (hard-capped at 200 due to model provider request limits). Playground batches are in-memory by default; set optional persistRuns to keep them across serve restarts. Optional maxPlaygroundRuns caps how many batches the playground / /v1/dev/evals* history keeps (default 20):

import {
  defineEvalConfig,
  persistEvalRunsToDir,
} from "@cursor/july/evals";

export default defineEvalConfig({
  maxConcurrency: 20,
  // Optional — playground /v1/dev/evals history window (default 20):
  // maxPlaygroundRuns: 50,
  // Optional — survive serve restarts (omit to keep runs in memory only):
  persistRuns: persistEvalRunsToDir(".agent-serve/eval-runs"),
});
agent-serve eval --dir . --list
agent-serve eval --dir . --json
agent-serve eval --dir . weather/nyc         # one datapoint
agent-serve eval --dir . weather             # whole file
agent-serve eval --dir . weather forecast    # multiple files
agent-serve eval --dir . --tag smoke

See docs/ for the full documentation set, AGENTS.md for the coding-agent guide, and skills/ for task-shaped guides (create an agent, author evals, hillclimb, GitHub webhooks, Slack setup, debugging).

Web playground

Every served agent ships with a built-in playground at http://127.0.0.1:3000/playground — a Vite + React SPA (reusing Cursor's internal UI components) over the same public HTTP API, made for quick manual testing and demo recordings:

  • chat with the agent and watch text/reasoning stream live, rendered as markdown (headings, lists, tables, blockquotes, links) with syntax highlighting for fenced code blocks (C-like languages, Python, shell, and diffs),
  • invoke custom channels as slash commands in the composer (e.g. a /drive https://github.com/org/repo/pull/1 channel route) — routes from GET /v1/info, with /help and autocomplete; same HTTP as the Agent surface Try buttons,
  • see tool calls inline (args, output, error state) as actions.requested / action.result events arrive,
  • browse every session (chat, custom-channel, and schedule task sessions) and replay their durable event streams,
  • dispatch schedules by hand in dev mode,
  • inspect the discovered agent surface (tools, skills, subagents, MCP connections, channels, hooks),
  • flip on the raw NDJSON pane to see the exact wire events.

The SPA is a static bundle. agent-serve serve auto-builds dist/playground/ when it is missing and the local vite toolchain is present (pnpm run build also emits it for publish). The server serves the bundle and every call it makes runs the normal route auth chain (there's a bearer-token field for non-loopback setups). Disable it with --no-playground (CLI) or serve(dir, { playground: false }). When serving many agents, each has its own playground at /<slug>/playground and / is an index of them all (see "Serving many agents at once").

Developing the playground

serve --dev / dev also starts Vite HMR (default :5273) and prints that URL as playground. Single-agent proxies /v1 to the serve URL; multi-agent serves the agents index at / and each SPA at /<slug>/playground:

# single agent — auto-build + HMR in one process
pnpm exec tsx src/bin/agent-serve.ts serve --dir ./my-agent --dev

# monorepo dev: multi-agent HMR
mise //packages/agent-serve:start
# → backend :3000, playground HMR :5273 (open /, then /<slug>/playground)

# pin one slug at the HMR root, or UI-only against an already-running serve
AGENT_SERVE_BASE=/my-agent mise //packages/agent-serve:start
AGENT_SERVE_MULTI=1 mise //packages/agent-serve:dev-playground

The Vite dev server proxies API calls to the backend (AGENT_SERVE_TARGET, default http://127.0.0.1:3000). Multi-agent HMR proxies /<slug>/v1/* as-is and serves each SPA at /<slug>/playground; a single-slug pin uses AGENT_SERVE_BASE=/<slug> at the HMR root. Editing anything under playground/src hot-reloads in the browser. The playground source lives in playground/ (entry playground/src/main.tsx); markdown rendering and the trace model are plain modules under playground/src/lib with unit tests.

To share the server beyond localhost (a tunnel, a LAN address, a phone), pass --bearer-token <secret> (or serve(dir, { authToken })). That replaces the loopback-only default with bearerAuth(secret) on every channel that doesn't author its own auth. The default localDevStrict() only admits direct loopback callers, rejects proxy-forwarding headers (X-Forwarded-For, X-Real-IP, Forwarded, X-Forwarded-Host), and requires a loopback Host header, so same-host reverse proxies / tunnels do not accidentally re-expose the default routes. Open the playground on the remote device and paste the token into the top-right field.

Session follow-up, stream, and list routes also bind to the principal that created the session (403 when a different admitted principal addresses someone else's handle). Session ids are restricted to a single safe path segment before they touch <stateRoot>/sessions.

How it runs on the Cursor harness

Every session is one Cursor SDK agent (Agent.create / Agent.resume). Local agents use the session id as the SDK agent id; cloud agents persist a separate sdkAgentId (typically bc-…). Each session gets its own workspace directory, materialized from the authored files and handed to the local harness as its working directory:

| Folder or file | Runtime mapping | | ------------------------ | ------------------------------------------------------------------------------- | | instructions.* | Local: AGENTS.md in the session workspace. Cloud: prepended to the first prompt. | | tools/*.ts (execution: "server", default) | Local only — in-process SDK custom tools. Not available on cloud. | | tools/*.ts (execution: "agent") | Local: scripts under .agent-serve/tools/ + catalog in AGENTS.md. Cloud: catalog + script bodies on the first prompt. | | skills/* | Local: .cursor/skills/<name>/SKILL.md in the workspace. Cloud: only if present in the cloud repo. | | mcp-connections/*.ts | Always three places: Cursor agent via SDK mcpServers (local + cloud), host-side ctx.host.mcp for in-process tools, and args.host.mcp on channel/schedule handlers. | | subagents/<id>/ | SDK custom subagents (the model delegates via the harness task tool) | | sandbox/workspace/** | Local session workspace seed on first turn; ignored for cloud runtime. | | channels/, schedules/, hooks/ | Served by this framework around the harness |

Conversation state for local agents persists through the SDK's local store under .agent-serve/runner/, and every session's event stream is recorded to .agent-serve/sessions/<id>/events.ndjson — sessions survive server restarts, and streams replay from any startIndex.

Folder structure

agent.ts

import { defineAgent } from "@cursor/july";

export default defineAgent({
  model: {
    id: "grok-4.5",
    params: [
      { id: "effort", value: "high" },
      { id: "fast", value: "true" },
    ],
  }, // optional; this is the default
  runtime: "local", // default — or "cloud"
  // cloud: {
  //   repos: [{ url: "https://github.com/org/repo", startingRef: "main" }],
  //   env: { type: "cloud" },
  // },
});

model accepts a Cursor model id string or { id, params }. When omitted on the root agent, agent-serve defaults to grok-4.5 with effort=high and fast=true.

runtime selects where turns execute:

| Value | Behavior | | --------- | ------- | | "local" | Cursor SDK local harness on this machine. Session id doubles as the SDK agent id. Authored tools, skills, and sandbox seeds apply. | | "cloud" | Cursor cloud agents. Pass a cloud block (repos, env, envVars, …) forwarded to the SDK. In-process server tools are not available; instructions and agent-tool catalogs are prepended to the first prompt because the local session workspace is not the cloud VM. |

Discovery warns when runtime: "cloud" is combined with server tools, skills, or sandbox seeds that only apply locally.

Instructions

agent/instructions.md is the always-on system prompt (required on the root agent). When the prompt needs to be generated, use agent/instructions.ts with defineInstructions({ markdown }) or a plain string default export, or split prose across an agent/instructions/ directory (composed in filename order).

Tools (agent/tools/*.ts)

One file per tool; the filename is the tool name the model sees.

execution chooses where the tool body runs:

| Value | Behavior | | ----- | -------- | | "server" (default) | In-process on the agent-serve host via Cursor SDK custom tools. Requires execute. Only available when the agent runtime is "local". | | "agent" | Materialized into the agent environment as a shell script. Requires script (JSON on stdin, result on stdout). Works with local and cloud agent runtimes. |

Server tool (default):

import { defineTool } from "@cursor/july/tools";
import { z } from "zod";

export default defineTool({
  description: "Get the current weather for a city.",
  // execution: "server", // default
  inputSchema: z.object({ city: z.string() }),
  async execute({ city }, ctx) {
    return { city, condition: "Sunny", temperatureF: 72 };
  },
});

Human-in-the-loop: set needsApproval: true (or an (input) => boolean predicate on validated args) so the host parks the in-flight tool call until a human approves or denies. The turn stays running; the stream emits action.approval_requested / action.approval_resolved.

Resolve surfaces (park is always on when needsApproval is set):

  • Playground Approve / Deny buttons
  • HTTP: POST /v1/session/:sessionId/approvals/:callId with {"decision":"approve"|"deny"}
  • Slack (opt-in channel surface) — set toolApprovals: true on slackChannel, and enable interactivity in the Slack app manifest:
import { slackChannel } from "@cursor/july/channels/slack";

export default slackChannel({
  credentials: { /* … */ },
  toolApprovals: true, // posts Block Kit Approve/Deny + routes clicks
});

Or compose the builders yourself:

import {
  buildDefaultEvents,
  buildToolApprovalEvents,
  slackChannel,
} from "@cursor/july/channels/slack";

export default slackChannel({
  deliverDefaults: false,
  // Required so Socket Mode routes Approve/Deny clicks when composing
  // buildToolApprovalEvents by hand (toolApprovals: true also enables this).
  interactivity: true,
  events: {
    ...buildDefaultEvents({ credentials }),
    ...buildToolApprovalEvents({ credentials }),
  },
});

In --dev, the playground can list and stream Slack sessions, and Approve/Deny parked tools on those sessions (audit by records the HTTP caller). That bridge does not open cross-owner approval for HTTP sessions. Production and bearer-auth hosts stay strict: Slack resolve must come from Slack interactivity (or a matching principal).

--allow-anonymous is for trusted-network demos only — every HTTP caller shares the same anonymous principal. Prefer --bearer-token when the host is shared.

Slack cards show redacted / truncated args for Block Kit limits; execution still uses the full validated tool input. Review sensitive tools in the playground or a private surface when args may exceed the card.

Example tool:

export default defineTool({
  description: "Post a weather alert to ops.",
  needsApproval: true,
  inputSchema: z.object({
    city: z.string(),
    message: z.string(),
  }),
  async execute({ city, message }) {
    return { posted: true, city, message };
  },
});

Approvals are only supported for execution: "server" tools on the local runtime. Exact resume of a parked SDK tool call does not survive host process restart — pending approvals left after a crash are treated as interrupted. Agent tool (runs where the Cursor agent runs — local harness or cloud VM):

import { defineTool } from "@cursor/july/tools";
import { z } from "zod";

export default defineTool({
  description: "Echo a message from the agent workspace.",
  execution: "agent",
  inputSchema: z.object({ message: z.string() }),
  script: `#!/usr/bin/env bash
set -euo pipefail
message=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["message"])')
printf '%s\\n' "$message"
`,
});

inputSchema is a zod schema (validated and typed for server tools) or a plain JSON Schema object (forwarded as-is). For server tools, ctx carries { toolCallId, session, workspaceDir }. Return a string, a JSON value, or { content: [...], isError? } for rich results.

Deterministic tool calls

Server tools can also be called deterministically — you pick the tool and the input, no model turn decides anything. The input is validated against the tool's schema and execute runs in-process; the result comes back exactly as the model would receive it. No Cursor API key is needed.

Over HTTP (POST /v1/tools/:toolName, same auth chain as the session API):

curl -X POST http://127.0.0.1:3000/<slug>/v1/tools/get_weather \
  -H 'content-type: application/json' \
  -d '{"input":{"city":"NYC"}}'
# {"ok":true,"toolName":"get_weather","callId":"tool_get_weather_...",
#  "isError":false,"result":{...},"durationMs":12}

From the CLI (boots an ephemeral server unless --url targets a running one):

agent-serve call get_weather --dir . --input '{"city":"NYC"}'
agent-serve call get_weather --url http://127.0.0.1:3000/<slug> --input '{"city":"NYC"}'

Programmatically, callTool(toolName, input, options?) is available on the serve handle, on channel route handlers and onStart args, and on schedule run handlers — so a channel can mix deterministic tool calls with model turns (e.g. fetch PR metadata deterministically, then send() the review prompt):

const outcome = await handle.callTool("get_weather", { city: "NYC" });
// { toolName, callId, isError, result, durationMs }

By default the tool runs against an ephemeral scratch workspace under <stateRoot>/tool-calls/<callId> with a synthetic direct session context — materialized like a session workspace (AGENTS.md, skills, seed files) and removed once the call returns. Pass sessionId (body field over HTTP, --session on the CLI, options.sessionId programmatically) to run it inside an existing session instead: the tool sees that session's info and materialized workspace, and the call is recorded on the session's event stream as actions.requested / action.result under a per-call turnId — visible in the playground, NDJSON trace, and trajectories like any model-initiated call. Session-bound calls are serialized with model turns: while a turn is running the call is rejected with 409 session_busy.

Unknown tools are rejected with the available tool names, execution: "agent" tools cannot be called on the host (400), schema-invalid input is a 400 before the tool body runs (zod schemas validate; plain JSON Schema inputs pass through unvalidated, matching the model path), and a tool body that throws is reported as isError: true with the same { content, isError } envelope the model would see.

Skills (agent/skills/*)

Skills follow the SKILL.md convention: model-loadable procedures the harness advertises by description and loads on demand. Author them as flat markdown (skills/forecast.md, optional description frontmatter — the first body line is the fallback), packaged directories (skills/research/SKILL.md plus references/…, which require description frontmatter), or TypeScript (defineSkill from @cursor/july/skills) when content must be generated.

MCP connections (agent/mcp-connections/*.ts)

import { defineConnection } from "@cursor/july/connections";

export default defineConnection({
  url: "https://mcp.linear.app/mcp",
  headers: { authorization: `Bearer ${process.env.LINEAR_TOKEN}` },
});

The filename becomes the MCP server name. { command, args, env, cwd } declares a local stdio server instead, and { agent: "<slug>" } declares a peer MCP connection to another agent mounted on the same serve host (see "Agent-to-agent: every agent is an MCP server").

{ cursorAccount: true } declares a Cursor account MCP connection: the agent gets the MCP connectors the signed-in Cursor account already authorized (dashboard → MCP) with zero token plumbing. Every tool executes on the Cursor backend with the account's stored OAuth credentials; raw tokens never reach the serve host, session workspaces, or traces. Omit servers, or pass servers: "*", to forward every connected HTTP/SSE connector. Pass servers: ["Linear", ...] to expose only named connectors.

// agent/mcp-connections/cursor.ts: every connected connector
export default defineConnection({ cursorAccount: true });
// same, spelled out:
export default defineConnection({ cursorAccount: true, servers: "*" });
// only Linear:
export default defineConnection({ cursorAccount: true, servers: ["Linear"] });

Requirements and behavior:

  • The host must be signed in (agent-serve login or CURSOR_API_KEY); serve fails fast at startup otherwise, and logs each connector's live status (connected / needsAuth / error) as it starts.
  • Local turns and host-side calls go through a loopback bridge route guarded by a per-boot secret. Cloud-runtime turns reach the same bridge through the serve --public-url (so servers filters apply there too); without one, cloud turns fall back to the account's natively hydrated connectors (unfiltered) and the serve host logs why.
  • Backend execution covers the account's HTTP/SSE servers; stdio servers cannot run server-side (author a { command } MCP connection for those).
  • Exposure: whoever can talk to the agent can drive these connectors (they are ordinary agent tools). serve refuses to start when --allow-anonymous is combined with Cursor account MCP connections; use --bearer-token on shared hosts.

Every MCP connection is always available in three places:

  1. the Cursor agent (local or cloud), via SDK mcpServers
  2. the agent-serve host, for in-process tools via ctx.host.mcp
  3. channel / schedule handlers, via args.host.mcp (deterministic — no agent loop)
export default defineTool({
  description: "Search Linear issues.",
  inputSchema: z.object({ query: z.string() }),
  async execute({ query }, ctx) {
    return ctx.host.mcp.callTool("linear", "list_issues", { query });
  },
});
// agent/channels/webhook.ts — call MCP directly from a webhook
POST("/sync", {
  bodySchema: z.object({}),
  handler: async (_req, { host }) => {
    const result = await host.mcp.callTool("linear", "list_issues", {});
    return Response.json(result);
  },
});

host.mcp.names() lists MCP connection names; listTools(name) and callTool(name, tool, args) open the client lazily on first use.

Subagents (agent/subagents/<id>/)

A subagent is its own directory with the same agent.ts + instructions.md shape. description is required — the parent model reads it to decide when to delegate — and model is optional (inherit by default). On the Cursor harness, subagents run as SDK custom subagents: they inherit the parent's execution surface, so per-subagent tools/, skills/, and mcp-connections/ are reported as warnings and ignored for now.

Channels (agent/channels/*.ts)

The built-in HTTP channel is always mounted (under /<slug> in the default multi-agent layout; at the server root with mode: "single"):

  • POST /v1/session — start a session ({"message": "..."}; returns sessionId + continuationToken)
  • POST /v1/session/:sessionId — follow-up ({"message", "continuationToken"}; rotates the token; works for any chat session including custom channels like drive; 409 on stale tokens, busy sessions, or task/schedule sessions; 403 if the caller is not the session owner)
  • GET /v1/session/:sessionId/stream?startIndex=N — replay + live NDJSON (same owner check)
  • GET /v1/session/:sessionId/approvals — pending human-in-the-loop tool approvals for the session
  • POST /v1/session/:sessionId/approvals/:callId — approve or deny ({"decision":"approve"|"deny"})
  • GET /v1/sessions — sessions owned by the calling principal
  • POST /v1/tools/:toolName — call a server tool deterministically ({"input": {...}, "sessionId"?}; see "Deterministic tool calls")
  • GET /v1/health, GET /v1/info — liveness and the manifest snapshot Author agent/channels/http.ts only to override its defaults:
import {
  bearerAuth,
  httpChannel,
  localDevStrict,
} from "@cursor/july/channels";

export default httpChannel({
  auth: [localDevStrict(), bearerAuth(process.env.AGENT_TOKEN ?? "")],
  onMessage: (message, { auth }) =>
    `[caller ${auth?.principalId ?? "anonymous"}] ${message}`,
});

Custom channels declare their own routes (mounted under /v1/channels/<id>), observe stream events for the sessions they own, and control their continuation-token format (e.g. a thread id):

import { defineChannel, POST } from "@cursor/july/channels";
import { z } from "zod";

export default defineChannel({
  routes: [
    POST("/message", {
      description: "Enqueue a chat turn on this channel",
      bodySchema: z.object({
        message: z.string(),
        thread: z.string().optional(),
      }),
      handler: async (_req, { send, body }) => {
        const { message, thread } = body;
        // `auth` defaults to the request principal resolved by the auth chain.
        const session = await send(message, {
          continuationToken: thread, // stable key: same thread, same session
        });
        return Response.json({ sessionId: session.id });
      },
    }),
  ],
  events: {
    "message.completed"(event, channel, ctx) {
      // deliver the reply back to the surface that owns this channel
    },
  },
});

GET requires a Zod querySchema and POST / PUT / PATCH require a Zod bodySchema at compile time — plain JSON Schema objects will not type-check. Use z.object({}) or z.unknown() when the surface is intentionally open. Schemas are validated by the host before the handler runs (handlers get typed args.body / args.query) and projected on GET /v1/info so the playground Agent surface can Try the route, and so the composer can offer matching slash commands (e.g. /drive).

Route handlers receive a Fetch Request and helpers: send, getSession, receive (cross-channel hand-off), params, requestIp, auth, host (shared host services — MCP / GitHub / Slack; same as tool ctx.host), and waitUntil. Channel state declares initial per-session adapter state, persisted across events; handlers receive it on channel.state.

Auth: every route runs an auth-policy chain (auth on the channel). The default is [localDevStrict()] — direct loopback callers only, with proxy-forwarding headers and non-loopback Host rejected — so nothing is exposed publicly until you add real auth (bearerAuth(...), a custom policy, or the explicit allowAll()).

Slack (@cursor/july/channels/slack): a platform channel pack that defaults to Socket Mode. Author agent/channels/slack.ts with slackChannel():

import { slackChannel } from "@cursor/july/channels/slack";

// Single agent — SLACK_BOT_TOKEN + SLACK_APP_TOKEN
export default slackChannel();

// Multi-agent serve — one Slack app (and token pair) per agent
export default slackChannel({ envPrefix: "WEATHER_AGENT" });
// → WEATHER_AGENT_SLACK_BOT_TOKEN + WEATHER_AGENT_SLACK_APP_TOKEN

// Cursor account connection — no dedicated Slack app. `@Cursor Weatherbot …`
// routes here; replies post as "Weatherbot" through the Cursor Slack app.
export default slackChannel({ cursorAccount: true, agentName: "Weatherbot" });

Transport uses @slack/socket-mode + @slack/web-api. Socket Mode starts on channel mount when both tokens are present; otherwise the channel stays idle (channel idle … missing credentials) so multi-agent serve can mount agents that do not have Slack tokens configured yet.

The pack dispatches via waitUntil, streams assistant text (chat.startStream / appendStream / stopStream) with postMessage fallback, shows rotating status

  • tool thinking steps, and sets thread titles / suggested prompts. Handlers may return a prepared message / workspaceFiles / cloud to host-prepare PR reviews or attach cloud repos from an @mention.

Engagement: by default the agent is summoned, never proactive — it dispatches only on app_mention and DMs. Channel watch is an explicit opt-in:

export default slackChannel({
  envPrefix: "TRIAGE",
  engagement: {
    // mentions / directMessages default to true
    channelPosts: {
      allow: ["#triage-alerts"], // explicit allowlist only; no wildcard
      posts: "top-level",       // default: never dispatch on thread replies
      debounceMs: 15_000,       // optional: let rapid edits settle
    },
  },
  onChannelPost: async (ctx, message) => {
    // Same contract as onAppMention: return null to skip.
    return message.markdown.length > 20 ? {} : null;
  },
});

Watched posts dispatch with the same thread-scoped principal as mentions (so a later @mention continues the session) plus an engagement: "channel_post" auth attribute. Posts that mention the bot are left to the app_mention path; bot-authored posts never dispatch; deleting a post inside the debounce window cancels its dispatch. The Slack app must subscribe to message.channels / message.groups (agent-serve slack init --channel-posts, or add the events to an existing app) and be a member of each watched channel. Watched channels surface in info via meta.slackChannelPosts.

HITL compose: park/resume is independent (needsApproval on tools). Opt in to Slack delivery with toolApprovals: true (or spread buildToolApprovalEvents into events and set interactivity: true on slackChannel so Approve/Deny buttons resolve). That posts Block Kit Approve/Deny cards and routes Socket Mode interactive clicks to resolveApproval. Enable interactivity: true on buildSlackManifest when you use that surface. Threads bind with continuationToken = channelId:threadTs. Dev and prod are separate Slack apps. Requires SLACK_BOT_TOKEN + SLACK_APP_TOKEN (App-Level Token with connections:write).

Most demos under examples/ use the Cursor Slack connection (cursorAccount: true) with a unique single-token agentName@Cursor Benny …, @Cursor Bugbot …, @Cursor ApprovalBuddy …, etc. Sign the host in, then mention the agent; no per-agent Slack app required for chat. Agents that need channel watch or tool approvals keep a Socket Mode app in a second channel file (e.g. slack-app.ts) with their own env prefix (<PREFIX>_SLACK_BOT_TOKEN + <PREFIX>_SLACK_APP_TOKEN, derived from the directory name by slack init).

| Agent | @Cursor name | Optional Socket Mode app | | --- | --- | --- | | weather-agent | Weather | slack-app.ts (WEATHER_AGENT_SLACK_*, tool approvals) | | slack-agent | SlackAgent | — | | bugbot | Bugbot | — | | fsd | FSD | — | | benny | Benny | slack-app.ts (BENNY_SLACK_*, channel watch) | | approval-buddy | ApprovalBuddy | — |

PR-oriented demos (bugbot, fsd) extract a GitHub PR URL / owner/repo#N from the mention and run the same host path as their HTTP channels. security-reviewer is GitHub-task-only (no Slack channel); concierge is agent-to-agent only (no Slack channel).

For Socket Mode apps (channel watch / approvals): generate manifests → create apps → install + mint app token → env → agent-serve slack doctor --prefix …serve.

CLI (agent-serve slack …; guided setup in skills/setup-slack/SKILL.md):

agent-serve slack setup
agent-serve slack init --dir ./my-agent --name "My Agent"
agent-serve slack doctor --prefix MY_AGENT

init / manifest write .agent-serve/slack/manifest.{dev,prod}.json and env.example.

GitHub (@cursor/july/channels/github): a webhook channel pack (Eve-compatible dispatch). Author agent/channels/github.ts with githubChannel() and declare inbound hooks:

import { defaultGitHubAuth, githubChannel } from "@cursor/july/channels/github";

export default githubChannel({
  botName: "my-agent", // or GITHUB_APP_SLUG
  cursorAccount: {
    repos: ["owner/repo"],
    // permissions?: "read" | "pr-write" | "contents-write" (default pr-write)
  },
  onPullRequest: (ctx, pr) =>
    pr.action === "opened" ? { auth: defaultGitHubAuth(ctx) } : null,
  onCheckSuite: (ctx, suite) =>
    suite.conclusion === "failure" ? { task: () => triage(ctx) } : null,
});

Mounts at POST /<slug>/v1/channels/github. When a webhook secret is set the channel verifies X-Hub-Signature-256 before parsing (the HMAC becomes the request auth); without one it stays loopback-only (localDevStrict()) — except under serve --dev, which admits unsigned loopback deliveries so gh webhook forward and fixtures work with zero config. Hooks return { auth } to start a model turn as the actor, { task } for host-side work (202 ACK, runs past GitHub's ~10s timeout), or null to skip.

Testing GitHub agents locally. cursorAccount reuses agent-serve login for both Cursor SCM events and a refreshing repo-scoped GitHub credential used by ctx.github, ctx.host.github, and child gh commands. Default permissions: "pr-write" covers comments without contents:write; opt up to "contents-write" only when the agent must push. Without cursorAccount, API auth prefers GitHub App installation tokens when GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY (and an installation id) are set, then falls back to GITHUB_TOKEN / GH_TOKEN or gh auth login. The channel publishes the webhook events it dispatches on (derived from the declared hooks, or pinned via webhookEvents), so agent-serve github … can forward live deliveries with zero hand-listing — it wraps gh webhook forward:

# One-time: gh + the cli/gh-webhook extension
agent-serve github doctor --install

# Terminal A: serve the agent (dev accepts unsigned loopback deliveries)
agent-serve serve --dir ./my-agent --dev

# Terminal B: forward this repo's deliveries to the discovered github channel
# (URL + events auto-derived; repo inferred from the git remote). No secret
# needed against a --dev server; set GITHUB_WEBHOOK_SECRET to verify signatures.
agent-serve github forward --dir ./my-agent

# Inspect what would be forwarded (URL + events per agent)
agent-serve github events --dir ./agents --json

# Forward to EVERY discovered github channel at once
agent-serve github forward --dir ./agents

When several channels match (e.g. a folder of agent projects with more than one github channel), one forwarder fans out to all of them: gh webhook forward runs against a local proxy that re-posts each raw (still-signed) delivery to the channels whose event set matches. This is required — GitHub allows only one forwarder per repo, and gh webhook forward targets a single URL, so N processes would collide with Hook already exists.

gh webhook forward needs admin on the repo (or org owner for --org) — it registers a real webhook — and authenticates its relay with the GitHub CLI's own login. If GITHUB_TOKEN / GH_TOKEN is set in your env, deliveries fail with HTTP 401 (the relay rejects env tokens); blank it for the command (GITHUB_TOKEN= GH_TOKEN= agent-serve github forward …) or unset it. agent-serve github doctor / forward warn when they detect this.

Pass --repo owner/repo / --org ORG to override the inferred target, --events a,b,c to narrow the set, --url for a custom endpoint (e.g. a tunnel), and --slug / --channel to forward to just one of several agents. Against a --dev server no secret is needed; set GITHUB_WEBHOOK_SECRET (or --secret) to exercise signature verification (required for a non-dev target). Only one forwarder per repo/org at a time (a GitHub limitation). Fixture replay still works too — POST a saved payload with an x-github-event header (no signature needed in --dev).

No admin? Hillclimbing? Use github replay. gh webhook forward needs repo admin and a live event. agent-serve github replay <pr_url> instead reads the PR (pull access is enough — no admin, no relay, and GITHUB_TOKEN is fine) and synthesizes GitHub-shaped payloads it POSTs straight at the channel:

# Replay a pull_request delivery for a PR to the discovered channel
agent-serve github replay https://github.com/owner/repo/pull/123 --dir ./my-agent

# Replay everything the channel listens for (its declared events), CI failing
agent-serve github replay owner/repo#123 --dir ./my-agent --events '*' --conclusion failure

# Inspect payloads without POSTing (and snapshot them as reusable fixtures)
agent-serve github replay owner/repo#123 --dir ./my-agent --events '*' --dry-run --out fixtures/github

--events defaults to pull_request (* = the channel's declared events); --action / --conclusion / --comment / --context tune each synthesized event; --secret (or GITHUB_WEBHOOK_SECRET) signs them so a secret-configured channel verifies. Because replay sends a clean, signed loopback request, it passes both the localDevStrict() and allowAll()+signature auth modes — and it's fully deterministic, which is what hillclimbing wants.

Production alternative: pull from Cursor (--cursor-events). If the Cursor GitHub App is on the repo, serve --cursor-events --repo owner/repo long-polls /v0/scm-events with the host's Cursor account (no public URL / repo admin). Offset + consumer id live under <state-root>/cursor-events/. The stream is read as your Cursor user, so the flag requires a signed-in host (agent-serve login, CURSOR_API_KEY, or serve({ apiKey })) — serve fails fast rather than starting with a relay that can never receive events.

Hooks (agent/hooks/*.ts)

Observe-only subscribers that run after each event is recorded — audit logs, metrics, mirroring transcripts into your own store. Keys are event types (or *); handler errors are logged, never fatal.

Schedules (agent/schedules/*)

Markdown form (fire-and-forget task session):

---
cron: "0 9 * * 1-5"
---

Pull open incidents and post a summary to the metrics endpoint.

Handler form (full control, hand off to a channel):

import { defineSchedule } from "@cursor/july/schedules";
import webhook from "../channels/webhook.js";

export default defineSchedule({
  cron: "*/30 * * * *",
  async run({ receive, waitUntil, appAuth, mcp }) {
    // optional: await mcp.callTool("units", "celsius_to_fahrenheit", { value: 0 });
    waitUntil(
      receive(webhook, {
        message: "Check for new critical alerts. Report only when there are any.",
        auth: appAuth,
      })
    );
  },
});

Cron expressions are standard 5-field, evaluated in UTC with minute granularity. In production mode (agent-serve serve) schedules fire on cadence; in dev mode (--dev) they never fire automatically — dispatch one by hand, exactly once, through the same path production uses:

curl -X POST http://127.0.0.1:3000/<slug>/v1/dev/schedules/heartbeat
# {"scheduleId":"heartbeat","sessionIds":["ses_..."]}

Sessions, events, and streaming

Two handles do two jobs: the continuation token resumes a conversation (owned by the channel; one active continuation per session, stale tokens rejected), and the session id streams and inspects it (owned by the runtime).

Reminders

Per-session durable wakes (distinct from deploy-time agent/schedules/):

await handle.createReminder({
  purpose: "ci_recheck",
  channelId: "drive",
  continuationToken: "pr-owner-repo-1",
  delay: "2h",
  prompt: "Re-check CI. Only act if still failing.",
  until: "Cancel once CI is green or the PR is merged.",
});

Host/policy packs may pass run (return stop / skip / delivered) instead of prompts. In --dev, use POST /v1/dev/reminders/:id (or handle.dispatchReminder) to fire; auto-timers follow ServeOptions.reminders (default !dev). Run handlers are in-memory — after restart those reminders are disarmed (handler_lost_on_restart); re-arm from enroll/policy.

The NDJSON stream vocabulary — one JSON object per line, each carrying { type, index, sessionId, turnId?, at, data }:

| Event | Meaning | | --------------------- | -------------------------------------------------------------- | | session.started | A durable session was created. | | agent.bound | Cursor agent id is known (sdkAgentId; cloud: bc-… + URL). | | ab.assigned | Sticky A/B enrollment (experiment, variant or null skip). | | message.received | An inbound user message was accepted. | | turn.started | A turn began. | | step.started / step.completed | Model step boundaries (with duration). | | reasoning.appended / reasoning.completed | Reasoning deltas and the block end. | | message.appended | Assistant text delta (with cumulative text so far). | | message.completed | A finalized assistant text block (finishReason: stop or tool_call). | | actions.requested | The model requested a tool call (streams before execution). | | action.approval_requested | A needsApproval tool is parked awaiting a human. | | action.approval_resolved | Human approved or denied the parked tool call. | | action.result | A tool call returned (output, isError). | | subagent.called / subagent.completed | Delegation to a subagent. | | turn.completed | The turn finished (result, usage). | | turn.failed | The turn failed (message). | | session.waiting | The session parked, ready for the next message. | | session.completed / session.failed | Terminal states for task-mode (schedule) sessions. |

The stream is durable: reconnect with ?startIndex=<n> to replay from any point, or from 0 to rewind the whole session.

Serving programmatically

import { serve } from "@cursor/july";

const handle = await serve("./my-agent", {
  port: 3000,
  apiKey: process.env.CURSOR_API_KEY, // optional — see credential order below
});
console.log(`listening on ${handle.url}`);
// handle.dispatchSchedule("heartbeat"), handle.project, await handle.close()

The Cursor credential resolves in one order everywhere (SDK turns, cloud runtime, Cursor account MCP connections): explicit apiKey option / --api-keyCURSOR_API_KEY → the key stored by agent-serve login. whoami shows which one is active; logout removes the stored key (revoke it in the Cursor dashboard to kill it outright).

Local-dev note: login/account RPCs honor CURSOR_API_BASE_URL while the Cursor SDK harness honors CURSOR_BACKEND_URL. When pointing at a non-production backend, set both to the same URL — a key minted on one backend is rejected by the other.

serve refuses to start when discovery produced error diagnostics; run agent-serve validate (or read project.diagnostics) to see why.

State lives under <project>/.agent-serve/ (override with stateRoot / --state-root): sessions/<id>/{session.json,events.ndjson,workspace/} plus the SDK conversation store under runner/. Delete a session directory to forget that conversation.

Session workspaces are real Cursor project directories, so the harness also loads ambient project config from ancestor directories (nested AGENTS.md / .cursor rules and skills). That's usually what you want when the agent project is its own repository — but when it sits inside a large monorepo, either set local: { cwd } on defineAgent to a directory outside the monorepo (each session uses <cwd>/<sessionId>), or point --state-root somewhere outside (e.g. under /tmp or XDG state) so sessions don't inherit the monorepo's rules into context.

Not supported (yet)

Deliberately out of scope for now, and reported as warnings where the corresponding folder exists: per-subagent tools/skills/mcp-connections, nested subagents, WebSocket channel routes, platform channel packs for Discord and Teams (Slack is supported via @cursor/july/channels/slack; other platforms still use the authored defineChannel webhook form), OAuth-brokered MCP connections beyond the Cursor account ({ cursorAccount: true } covers connectors the account already authorized in Cursor), custom sandbox backends (sandbox.ts), instrumentation modules, dynamic per-caller capabilities, structured output schemas, and file uploads. Human-in-the-loop tool approvals (needsApproval on execution: "server" tools) are supported; exact resume of a parked tool call after host restart is not. Filesystem evals (defineEval under evals/), live A/B metrics (defineAB under agent/ab), and the run / trajectory commands are supported; LLM-as-judge scoring and external reporters are not yet. Context compaction is handled by the Cursor harness rather than configured here.