Photon

by portel-dev

Not rated yet

About

A TypeScript framework that turns a single class into an MCP server, CLI tool, and web dashboard with a marketplace of 35 ready-made photons.

Explore

Photon is the fastest way to turn a small, verified TypeScript method into something humans can operate and agents can trust. Write the capability once; Photon derives the interfaces, contracts, and runtime behavior around it:

- MCP serverfor Claude, ChatGPT, Cursor, and agents
- Embedded app UIfor chat clients that support MCP app resources
- CLI toolfor scripts, demos, and automation
- Beam web interfacefor humans
- Web routes, schedules, webhooks, retries, state, and audit historywhen the capability grows into a production workflow

Photon is free and open source software released under theMIT license. Full documentation lives atphoton.portel.dev.

Related Portel project:NCPgives agents one natural MCP interface to discover and run tools across a whole tool ecosystem. Photon builds reliable agent-facing capabilities; NCP helps agents find and use them alongside every other MCP.

bun add -g @portel/photon photon new my-tool photon

That opens Beam, the generated human UI. Addphoton mcp install my-toolwhen you want the same capability inside Claude Desktop or another MCP client.

Interfaces are optional. Intent is mandatory.

The weather example is intentionally small: one TypeScript method, a few docblock tags, and one@uiHTML asset. Photon turns that into a CLI command, Beam UI, MCP tool, and embedded app surface for MCP app-capable chat clients. Claude Desktop can run it from a local stdio MCP command; ChatGPT developer mode can connect to the same Photon over a public HTTPS/mcpendpoint.

Follow the step-by-step tutorialor open the runnable example inexamples/weather-showcase. The tutorial also includes Beam, CLI, and a concept animation for the full transformation.

Photon is the modern dev stack for the agentic age: each photon is a small, auditable brick that can be used by humans, agents, schedulers, webhooks, and apps without rewriting the same capability for every interface.

That is the core idea:tiny trusted capabilities compose into larger systems. A photon can start as a helper method, become a CLI command, render as an app, run on a schedule, accept webhooks, and still expose a clean agent-readable contract.

// hello.photon.ts export default class Hello { greet(name: string) { return Hello, ${name}!; } }

That's a complete photon. From this single file you get:

$ photon cli hello greet --name Ada # CLI $ photon # Web UI at localhost:3008 $ photon mcp hello # MCP server for Claude, Cursor, etc.

No decorators. No registration. No server boilerplate. Just define the intent. Photon handles the rest.

From zero to an MCP server connected to Claude Desktop in three commands:

bun add -g @portel/photon photon new my-tool # Scaffolds ./my-tool.photon.ts in your CWD photon mcp install my-tool # Registers it in Claude Desktop's config # Restart Claude Desktop. Your tool is live.

Prefer the web dashboard? Skip step 3 and runphotoninstead — it opens Beam, the auto-generated UI.

bunx @portel/photon new my-tool bunx @portel/photon mcp install my-tool # pnpm users can use pnpm dlx instead: pnpm dlx @portel/photon new my-tool pnpm dlx @portel/photon mcp install my-tool

RequiresNode.js 20+. TypeScript is compiled internally; notsconfig.jsonneeded.

Where do photon files live?./(a project directory you cd into) or~/.photon/(global, auto-discovered). User settings persist under~/.photon/state/<photon>/. SeeWhere things live.

You write a TypeScript class. Methods are your capabilities. Types describe what's valid. Comments explain the intent. Photon reads all of it and generates three interfaces from one file. Same logic. Same validation. Same data.

analytics.photon.ts → Web UI (Beam) · CLI · MCP Server for AI

The more you express, the more Photon derives:

When you add a@param city {@pattern ^[a-zA-Z\s]+$}annotation, Beam validates it in the form, the CLI validates it before running, and the MCP schema enforces it for the AI. One annotation. Three consumers.

extends Photonis one shape. You can also injectPhotonas a constructor parameter when you already extend something else, or compose without inheritance — same API either way. CF resources reach the photon through a separateCloudflareinjection so portable photons stay portable. Seedocs/guides/PHOTON-INJECTION.md.

Beam is the web dashboard. Every photon becomes an interactive form automatically. Runphoton. That's the whole command.

The UI isfully auto-generatedfrom your method signatures: field types, validation, defaults, layouts. You never write frontend code. When you add a{@choice a,b,c}tag to a parameter, Beam renders a dropdown. When you mark a string as{@format email}, the field validates email format. The UI evolves as your code does.

When forms aren't the right interface for what you're building, you can replace Beam's auto-generated view with your own HTML. A global named after your photon is auto-injected (e.g.,analytics.onResult(data => ...)) — no framework required.window.photon.urlis also injected and resolves to the Beam base URL so your HTML can construct fetch paths correctly whether running locally or behind a reverse proxy.

Custom UIs follow theofficial MCP Apps Extensionand work across compatible hosts. See theCustom UI Guide.

Photons that declare HTTP routes with@get,@post,@put,@patch, or@deleteare shown in Beam as web apps. Routes support dynamic path segments (e.g.@get /items/:id) matched by specificity: literal segments win over parameters. Beam proxies requests to those routes and injects anx-photon-base-pathheader so the app can construct correct absolute paths regardless of where Beam is hosted.

Photon ships separate, tested MCP adapters: sessionful MCP 2025 over stdio and Streamable HTTP, plus stateless MCP2026-07-28release-candidate support over Streamable HTTP. See thecompatibility matrix and runnable clients, or runphoton doctor mcpagainst your installed runtime.

{ "mcpServers": { "analytics": { "command": "photon", "args": ["mcp", "analytics"] } } }

Paste into your AI client's config. Your photon is now an MCP server. Claude can call your methods. Cursor can call your methods. Any MCP-compatible host can call your methods.

The AI sees the same thing a human sees in Beam: the method names, the parameter descriptions from your JSDoc, the validation rules from your types. The JSDoc comment you wrote to document the tool for yourself is what Claude reads to decide when and how to call it.

The MCP tools themselves work withClaude Desktop,Claude Code,Cursor, and any MCP-compatible client. When your photon has a custom UI, clients that support the[MCP Apps Extensioncan render it natively, as shown in the weather proof above.

Here is how a photon grows. Each step adds one thing and gets multiple capabilities from it.

export default class Weather { / User-tunable knobs. Photon auto-generates a settings tool from this. / protected settings = { / Units for forecast values / units: 'metric', / Polling interval in seconds / pollIntervalSec: 300, }; async forecast(params: { city: string }) { const res = await fetch(...?units=${this.settings.units}); return await res.json(); } }

protected settingsis the canonical way to expose runtime knobs. Photon reads the JSDoc on each property, generates an MCPsettingstool with typed inputs, and persists user changes to~/.photon/state/<photon>/<instance>-settings.json. Inside methods,this.settingsis a read-only Proxy. To change a value, the user (or AI) calls the auto-generatedsettingstool.

Forsecretsthat should never be persisted in a settings file (API keys, tokens), use a constructor parameter instead. Photon maps the parameter name to an env var:

export default class Weather { constructor(private apiKey: string) {} // → WEATHER_API_KEY }

The constructor pattern is for primitives that come from.env. Theprotected settingspattern is for everything else, including any knob the user should be able to change at runtime without restarting.When in doubt, reach forsettings.*

export default class Weather { / User-tunable knobs. Photon auto-generates a settings tool from this. / protected settings = { / Units for forecast values / units: 'metric', / Polling interval in seconds / pollIntervalSec: 300, }; async forecast(params: { city: string }) { const res = await fetch(...?units=${this.settings.units}); return await res.json(); } }

protected settingsis the canonical way to expose runtime knobs. Photon reads the JSDoc on each property, generates an MCPsettingstool with typed inputs, and persists user changes to~/.photon/state/<photon>/<instance>-settings.json. Inside methods,this.settingsis a read-only Proxy. To change a value, the user (or AI) calls the auto-generatedsettingstool.

Forsecretsthat should never be persisted in a settings file (API keys, tokens), use a constructor parameter instead. Photon maps the parameter name to an env var:

export default class Weather { constructor(private apiKey: string) {} // → WEATHER_API_KEY }

The constructor pattern is for primitives that come from.env. Theprotected settingspattern is for everything else, including any knob the user should be able to change at runtime without restarting.When in doubt, reach forsettings.*

photon psis the operator surface for the daemon. Without arguments it prints a four-section snapshot — ACTIVE schedules, DECLARED-but- not-enrolled, WEBHOOKS, and ACTIVE SESSIONS.

photon ps # full snapshot photon ps --json # structured output for scripts photon ps --type active # one section only photon ps --base ~/Projects/kith # filter to one PHOTON_DIR

Two-step model.A@scheduledannotation in source isDECLAREDuntil enrolled. Enrollment is per-machine, persistent, and explicit:

photon ps enable newsletter:sendDigest # DECLARED → ACTIVE photon ps disable newsletter:sendDigest # ACTIVE → suppressed (survives restart) photon ps pause newsletter:sendDigest # stop firing without removing enrollment photon ps resume newsletter:sendDigest # undo pause photon ps history newsletter:sendDigest # last 20 firings: timestamp, status, error

For manual cron schedules without a@scheduledtag, use the Beam Pulse panel ("Add schedule") or callthis.schedule.create()from photon code.

this.schedule.create()(programmatic schedules) skips DECLARED and goes straight to ACTIVE. Seedocs/GUIDE.md#schedulingfor the full reference, the daemon state layout, and.photon-no-hostfor multi-host setups.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.