@portel/photon 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -140
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +102 -65
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +12 -2
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/playground-html.js +5 -5
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +17 -7
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/beam.bundle.js +722 -354
- package/dist/beam.bundle.js.map +3 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/daemon/server.js +62 -50
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +35 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown-utils.d.ts.map +1 -1
- package/dist/markdown-utils.js +2 -1
- package/dist/markdown-utils.js.map +1 -1
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +20 -3
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +3 -1
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +20 -4
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +45 -11
- package/dist/security-scanner.d.ts.map +1 -1
- package/dist/security-scanner.js +8 -2
- package/dist/security-scanner.js.map +1 -1
- package/dist/serv/index.d.ts +1 -1
- package/dist/serv/index.d.ts.map +1 -1
- package/dist/serv/index.js +6 -4
- package/dist/serv/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +69 -26
- package/dist/server.js.map +1 -1
- package/dist/shared/security.d.ts +79 -0
- package/dist/shared/security.d.ts.map +1 -0
- package/dist/shared/security.js +255 -0
- package/dist/shared/security.js.map +1 -0
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +10 -3
- package/dist/template-manager.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ A framework, runtime, and ecosystem. Batteries included.
|
|
|
14
14
|
[](https://nodejs.org)
|
|
15
15
|
[](https://modelcontextprotocol.io)
|
|
16
16
|
|
|
17
|
-
[Quick Start](#quick-start) · [
|
|
17
|
+
[Quick Start](#quick-start) · [Why Photon](#why-did-we-build-this) · [Beam UI](#beam) · [How It Works](#how-it-works) · [Docs](#documentation)
|
|
18
|
+
|
|
19
|
+
[](https://www.youtube.com/watch?v=FI0M8s6ZKv4)
|
|
18
20
|
|
|
19
21
|
</div>
|
|
20
22
|
|
|
@@ -22,7 +24,7 @@ A framework, runtime, and ecosystem. Batteries included.
|
|
|
22
24
|
|
|
23
25
|
## What Is This Thing?
|
|
24
26
|
|
|
25
|
-
So, here is the situation. You write a single TypeScript file. Just one. And somehow, through some dark magic I don
|
|
27
|
+
So, here is the situation. You write a single TypeScript file. Just one. And somehow, through some dark magic I don't fully understand either, you get three things at once:
|
|
26
28
|
|
|
27
29
|
1. **An MCP server** (so Claude or Cursor can use your tools).
|
|
28
30
|
2. **A CLI tool** (so you can run it from the terminal like a normal human).
|
|
@@ -36,20 +38,6 @@ It looks like this:
|
|
|
36
38
|
|
|
37
39
|
You just write the logic. Photon deals with the protocols, schemas, and the boring stuff that usually makes you question your life choices.
|
|
38
40
|
|
|
39
|
-
### The Basics
|
|
40
|
-
|
|
41
|
-
If you are just skimming, here is what you need to know:
|
|
42
|
-
|
|
43
|
-
| Concept | What it is | Learn more |
|
|
44
|
-
|---------|-----------|------------|
|
|
45
|
-
| **MCP** | A way for AI to use your tools. It’s a standard. | [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) |
|
|
46
|
-
| **Photon file** | A `.photon.ts` file. You define tools as methods in a class. | [Guide](./GUIDE.md) |
|
|
47
|
-
| **Beam** | A web dashboard. It shows your tools as forms. | [Beam UI](#beam) |
|
|
48
|
-
| **Marketplace** | A way to get other people’s photons. | [Marketplace](#marketplace) |
|
|
49
|
-
| **Daemon** | A background thing that handles messages and jobs. | [Daemon Pub/Sub](./DAEMON-PUBSUB.md) |
|
|
50
|
-
| **Tags** | JSDoc comments that tell Photon what to do. | [Tag Reference](./DOCBLOCK-TAGS.md) |
|
|
51
|
-
| **Custom UI** | When the auto-generated forms aren't enough. | [Custom UI Guide](./CUSTOM-UI.md) |
|
|
52
|
-
|
|
53
41
|
### Who Is This For?
|
|
54
42
|
|
|
55
43
|
* **Developers** who want to give AI access to their database but are too lazy to write a full server.
|
|
@@ -60,6 +48,18 @@ You don't need to know what "MCP" actually stands for. If you can write a TypeSc
|
|
|
60
48
|
|
|
61
49
|
---
|
|
62
50
|
|
|
51
|
+
## Why did we build this?
|
|
52
|
+
|
|
53
|
+
Three reasons, if you want the short version. ([Read the longer version](./WHY-PHOTON.md))
|
|
54
|
+
|
|
55
|
+
**MCP is personal.** The best MCP is the one built for exactly one use case. Yours. Your team's. Your company's. When you stop building for everyone, the code gets absurdly simple. One file. Twelve lines. Not twelve hundred.
|
|
56
|
+
|
|
57
|
+
**Solve once, run forever.** If an LLM figured out your workflow the first time, why ask it to re-derive the same answer from scratch every time? Photon lets you keep the answer. No middleman, no tokens, no latency.
|
|
58
|
+
|
|
59
|
+
**Same door, every key.** AI calls it through MCP. You call it through CLI. You open it in Beam. Same methods, same data, same result. And half the time, you don't need AI at all. You just need the data.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
63
|
## Quick Start
|
|
64
64
|
|
|
65
65
|
If you are the type who likes to just run commands and see what happens:
|
|
@@ -81,11 +81,69 @@ npx @portel/photon
|
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
|
+
## Beam
|
|
85
|
+
|
|
86
|
+
Beam is the dashboard. It's where you go to poke your tools and see if they work before you let an AI loose on them.
|
|
87
|
+
|
|
88
|
+
Run `photon`. That's it.
|
|
89
|
+
|
|
90
|
+
<div align="center">
|
|
91
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/beam-dashboard.png" alt="Beam Dashboard" width="100%">
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Connecting to AI
|
|
97
|
+
|
|
98
|
+
If you want to use this with Claude or Cursor, you need the config.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
photon info weather --mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It spits out some JSON:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"weather": {
|
|
110
|
+
"command": "photon",
|
|
111
|
+
"args": ["mcp", "weather"]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Copy that. Paste it into your AI client's config file. Done.
|
|
118
|
+
|
|
119
|
+
Works with [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.com), and any [MCP-compatible client](https://modelcontextprotocol.io).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Marketplace
|
|
124
|
+
|
|
125
|
+
We also have a marketplace. 35 photons and counting.
|
|
126
|
+
|
|
127
|
+
<div align="center">
|
|
128
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/beam-marketplace.png" alt="Marketplace" width="100%">
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
photon search postgres
|
|
133
|
+
photon add postgres
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Browse the full catalog and documentation in the [official photons repository](https://github.com/portel-dev/photons).
|
|
137
|
+
|
|
138
|
+
You can also make a private marketplace for your team, so internal tools stay off the public internet.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
84
142
|
## How It Works
|
|
85
143
|
|
|
86
144
|
A photon is just a TypeScript class. The **public methods become tools**. Photon reads your code, looks at the types, reads your comments, and then generates everything else.
|
|
87
145
|
|
|
88
|
-
I
|
|
146
|
+
I'll show you.
|
|
89
147
|
|
|
90
148
|
### Step 1: The Bare Minimum
|
|
91
149
|
|
|
@@ -107,7 +165,7 @@ export default class Weather {
|
|
|
107
165
|
* `photon` (The web UI)
|
|
108
166
|
|
|
109
167
|
<div align="center">
|
|
110
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-1.png" alt="Step 1 — Bare method in Beam" width="
|
|
168
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-1.png" alt="Step 1 — Bare method in Beam" width="100%">
|
|
111
169
|
</div>
|
|
112
170
|
|
|
113
171
|
### Step 2: Adding Descriptions
|
|
@@ -134,7 +192,7 @@ export default class Weather {
|
|
|
134
192
|
**What happens:** Now the UI has helpful text. Also, the AI client reads this to understand what the tool does.
|
|
135
193
|
|
|
136
194
|
<div align="center">
|
|
137
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-2.png" alt="Step 2 — JSDoc descriptions in Beam" width="
|
|
195
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-2.png" alt="Step 2 — JSDoc descriptions in Beam" width="100%">
|
|
138
196
|
</div>
|
|
139
197
|
|
|
140
198
|
### Step 3: Configuration (The clever bit)
|
|
@@ -160,7 +218,7 @@ export default class Weather {
|
|
|
160
218
|
**What happens:** Beam creates a settings panel. `apiKey` becomes a password field. It also maps to environment variables like `WEATHER_API_KEY`. It just works.
|
|
161
219
|
|
|
162
220
|
<div align="center">
|
|
163
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-3.png" alt="Step 3 — Configuration panel in Beam" width="
|
|
221
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-3.png" alt="Step 3 — Configuration panel in Beam" width="100%">
|
|
164
222
|
</div>
|
|
165
223
|
|
|
166
224
|
### Step 4: Validation (Stop bad inputs)
|
|
@@ -223,7 +281,7 @@ VideoProcessor requires the following CLI tools to be installed:
|
|
|
223
281
|
> See the full [Tag Reference](./DOCBLOCK-TAGS.md) for all available tags. There are 30+ covering validation, UI hints, scheduling, webhooks, and more.
|
|
224
282
|
|
|
225
283
|
<div align="center">
|
|
226
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-4.png" alt="Step 4 — Validation and formatting in Beam" width="
|
|
284
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-4.png" alt="Step 4 — Validation and formatting in Beam" width="100%">
|
|
227
285
|
</div>
|
|
228
286
|
|
|
229
287
|
### Step 5: Custom UI (When you want to be fancy)
|
|
@@ -267,7 +325,7 @@ export default class Weather {
|
|
|
267
325
|
> Custom UIs follow the [MCP Apps Extension (SEP-1865)](https://github.com/nicolo-ribaudo/modelcontextprotocol/blob/nicolo/sep-1865/docs/specification/draft/extensions/apps.mdx) standard and work across compatible hosts. See the [Custom UI Guide](./CUSTOM-UI.md).
|
|
268
326
|
|
|
269
327
|
<div align="center">
|
|
270
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-5.png" alt="Step 5 — Custom UI result in Beam" width="
|
|
328
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/readme-step-5.png" alt="Step 5 — Custom UI result in Beam" width="100%">
|
|
271
329
|
</div>
|
|
272
330
|
|
|
273
331
|
### In Summary
|
|
@@ -281,129 +339,24 @@ export default class Weather {
|
|
|
281
339
|
| **5. Custom UI** | HTML | A custom app |
|
|
282
340
|
|
|
283
341
|
<div align="center">
|
|
284
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/photon-ecosystem.png" alt="Photon Ecosystem" width="
|
|
342
|
+
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/photon-ecosystem.png" alt="Photon Ecosystem" width="100%">
|
|
285
343
|
</div>
|
|
286
344
|
|
|
287
345
|
---
|
|
288
346
|
|
|
289
|
-
##
|
|
290
|
-
|
|
291
|
-
Beam is the dashboard. It’s where you go to poke your tools and see if they work before you let an AI loose on them.
|
|
292
|
-
|
|
293
|
-
Run `photon`. That’s it.
|
|
294
|
-
|
|
295
|
-
<div align="center">
|
|
296
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/beam-dashboard.png" alt="Beam Dashboard" width="700">
|
|
297
|
-
</div>
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
## Connecting to AI
|
|
302
|
-
|
|
303
|
-
If you want to use this with Claude or Cursor, you need the config.
|
|
304
|
-
|
|
305
|
-
```bash
|
|
306
|
-
photon info weather --mcp
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
It spits out some JSON:
|
|
310
|
-
|
|
311
|
-
```json
|
|
312
|
-
{
|
|
313
|
-
"mcpServers": {
|
|
314
|
-
"weather": {
|
|
315
|
-
"command": "photon",
|
|
316
|
-
"args": ["mcp", "weather"]
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Copy that. Paste it into your AI client’s config file. Done.
|
|
347
|
+
## The Basics
|
|
323
348
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## Why did we build this?
|
|
329
|
-
|
|
330
|
-
Writing an MCP server usually involves 4 to 6 files and about 150 lines of code before you even start writing the thing you actually wanted to write.
|
|
331
|
-
|
|
332
|
-
With Photon, it’s one file.
|
|
333
|
-
|
|
334
|
-
| | Traditional MCP | Photon |
|
|
335
|
-
|---|---|---|
|
|
336
|
-
| **Files** | 4-6 (server, transport, schemas, types, config) | 1 |
|
|
337
|
-
| **Boilerplate** | 150+ lines | 0 |
|
|
338
|
-
| **Dependencies** | Manual `npm install` | Automatic |
|
|
339
|
-
| **Schema** | Hand-written JSON Schema | Generated from TS types |
|
|
340
|
-
| **Config** | Manual env var parsing | Automatic from Constructor |
|
|
341
|
-
|
|
342
|
-
It is unnecessarily difficult to do it the old way. So we stopped doing it.
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## Marketplace
|
|
347
|
-
|
|
348
|
-
We also have a marketplace. 31 photons and counting.
|
|
349
|
-
|
|
350
|
-
<div align="center">
|
|
351
|
-
<img src="https://raw.githubusercontent.com/portel-dev/photon/main/assets/beam-marketplace.png" alt="Marketplace" width="700">
|
|
352
|
-
</div>
|
|
353
|
-
|
|
354
|
-
```bash
|
|
355
|
-
photon search postgres
|
|
356
|
-
photon add postgres
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### Available Photons
|
|
360
|
-
|
|
361
|
-
**Productivity**
|
|
362
|
-
|
|
363
|
-
| Photon | What it does | Tools |
|
|
364
|
-
|--------|-------------|-------|
|
|
365
|
-
| 📌 **kanban** | Multi-tenant task boards for humans and AI | 33 |
|
|
366
|
-
| 📬 **git-box** | Mailbox-style Git interface, manage repos like an inbox | 58 |
|
|
367
|
-
| 📬 **form-inbox** | Webhook-powered form submission collector | 12 |
|
|
368
|
-
| 📅 **google-calendar** | Calendar integration via OAuth | 9 |
|
|
369
|
-
| 🎫 **jira** | Project management and issue tracking | 10 |
|
|
370
|
-
| 💬 **slack** | Send messages and manage Slack workspaces | 7 |
|
|
371
|
-
| 📧 **email** | Send and receive via SMTP/IMAP | 8 |
|
|
372
|
-
|
|
373
|
-
**Infrastructure**
|
|
374
|
-
|
|
375
|
-
| Photon | What it does | Tools |
|
|
376
|
-
|--------|-------------|-------|
|
|
377
|
-
| 📁 **filesystem** | Safe, cross-platform file operations | 13 |
|
|
378
|
-
| 🔀 **git** | Local git repository operations | 11 |
|
|
379
|
-
| 🐙 **github-issues** | Manage GitHub issues and comments | 7 |
|
|
380
|
-
| 🐳 **docker** | Container and image management | 10 |
|
|
381
|
-
| ☁️ **aws-s3** | S3 object storage operations | 11 |
|
|
382
|
-
| 🌐 **web** | DuckDuckGo search + Readability extraction | 2 |
|
|
383
|
-
|
|
384
|
-
**Databases**
|
|
385
|
-
|
|
386
|
-
| Photon | What it does | Tools |
|
|
387
|
-
|--------|-------------|-------|
|
|
388
|
-
| 🐘 **postgres** | PostgreSQL queries and schema ops | 7 |
|
|
389
|
-
| 🗄️ **sqlite** | SQLite database operations | 9 |
|
|
390
|
-
| 🍃 **mongodb** | MongoDB document CRUD and aggregation | 13 |
|
|
391
|
-
| ⚡ **redis** | Key-value store, lists, sets, pub/sub | 18 |
|
|
392
|
-
|
|
393
|
-
**Utilities and Demos**
|
|
394
|
-
|
|
395
|
-
| Photon | What it does | Tools |
|
|
396
|
-
|--------|-------------|-------|
|
|
397
|
-
| 🕐 **time** | Timezone conversion and queries | 3 |
|
|
398
|
-
| 🧮 **math** | Expression evaluator (trig, stats, etc.) | 1 |
|
|
399
|
-
| 📊 **code-diagram** | Generate Mermaid diagrams from code | 3 |
|
|
400
|
-
| 🔴 **connect-four** | Play against AI with distributed locks | 8 |
|
|
401
|
-
| 🍳 **kitchen-sink** | Every runtime feature in one file | 25 |
|
|
402
|
-
| 📋 **dashboard** | MCP Apps UI demo | 6 |
|
|
403
|
-
| 📺 **team-dashboard** | TV/monitor-optimized team display | 20 |
|
|
404
|
-
| 🎭 **mcp-orchestrator** | Combine multiple MCPs into workflows | 10 |
|
|
349
|
+
If you are just skimming, here is what you need to know:
|
|
405
350
|
|
|
406
|
-
|
|
351
|
+
| Concept | What it is | Learn more |
|
|
352
|
+
|---------|-----------|------------|
|
|
353
|
+
| **MCP** | A way for AI to use your tools. It's a standard. | [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) |
|
|
354
|
+
| **Photon file** | A `.photon.ts` file. You define tools as methods in a class. | [Guide](./GUIDE.md) |
|
|
355
|
+
| **Beam** | A web dashboard. It shows your tools as forms. | [Beam UI](#beam) |
|
|
356
|
+
| **Marketplace** | A way to get other people's photons. | [Marketplace](#marketplace) |
|
|
357
|
+
| **Daemon** | A background thing that handles messages and jobs. | [Daemon Pub/Sub](./DAEMON-PUBSUB.md) |
|
|
358
|
+
| **Tags** | JSDoc comments that tell Photon what to do. | [Tag Reference](./DOCBLOCK-TAGS.md) |
|
|
359
|
+
| **Custom UI** | When the auto-generated forms aren't enough. | [Custom UI Guide](./CUSTOM-UI.md) |
|
|
407
360
|
|
|
408
361
|
---
|
|
409
362
|
|
|
@@ -516,4 +469,3 @@ If you find a bug, or if my code offends you, feel free to open an issue or a PR
|
|
|
516
469
|
Made by [Portel](https://github.com/portel-dev)
|
|
517
470
|
|
|
518
471
|
</div>
|
|
519
|
-
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"beam.d.ts","sourceRoot":"","sources":["../../src/auto-ui/beam.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"beam.d.ts","sourceRoot":"","sources":["../../src/auto-ui/beam.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAixBH,wBAAsB,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0/ElF;AAiYD;;;GAGG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB9C"}
|
package/dist/auto-ui/beam.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as os from 'os';
|
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { createHash } from 'crypto';
|
|
17
|
+
import { isPathWithin, isLocalRequest, setSecurityHeaders, readBody, SimpleRateLimiter } from '../shared/security.js';
|
|
17
18
|
/**
|
|
18
19
|
* Generate a unique ID for a photon based on its path.
|
|
19
20
|
* This ensures photons with the same name from different paths are distinguishable.
|
|
@@ -178,8 +179,7 @@ async function loadExternalMCPs(config) {
|
|
|
178
179
|
try {
|
|
179
180
|
const resourcesResult = await sdkClient.listResources();
|
|
180
181
|
const resources = resourcesResult.resources || [];
|
|
181
|
-
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') ||
|
|
182
|
-
r.mimeType === 'application/vnd.mcp.ui+html');
|
|
182
|
+
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') || r.mimeType === 'application/vnd.mcp.ui+html');
|
|
183
183
|
// Count only non-UI resources (UI resources are internal implementation detail)
|
|
184
184
|
mcpInfo.resourceCount = resources.length - appResources.length;
|
|
185
185
|
if (appResources.length > 0) {
|
|
@@ -257,8 +257,7 @@ async function loadExternalMCPs(config) {
|
|
|
257
257
|
const resourcesResult = await sdkClient.listResources();
|
|
258
258
|
const resources = resourcesResult.resources || [];
|
|
259
259
|
// Check for MCP App resources (ui:// scheme or application/vnd.mcp.ui+html mime)
|
|
260
|
-
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') ||
|
|
261
|
-
r.mimeType === 'application/vnd.mcp.ui+html');
|
|
260
|
+
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') || r.mimeType === 'application/vnd.mcp.ui+html');
|
|
262
261
|
// Count only non-UI resources (UI resources are internal implementation detail)
|
|
263
262
|
mcpInfo.resourceCount = resources.length - appResources.length;
|
|
264
263
|
if (appResources.length > 0) {
|
|
@@ -358,8 +357,7 @@ async function reconnectExternalMCP(name) {
|
|
|
358
357
|
try {
|
|
359
358
|
const resourcesResult = await sdkClient.listResources();
|
|
360
359
|
const resources = resourcesResult.resources || [];
|
|
361
|
-
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') ||
|
|
362
|
-
r.mimeType === 'application/vnd.mcp.ui+html');
|
|
360
|
+
const appResources = resources.filter((r) => r.uri?.startsWith('ui://') || r.mimeType === 'application/vnd.mcp.ui+html');
|
|
363
361
|
// Count only non-UI resources (UI resources are internal implementation detail)
|
|
364
362
|
mcp.resourceCount = resources.length - appResources.length;
|
|
365
363
|
if (appResources.length > 0) {
|
|
@@ -671,8 +669,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
671
669
|
defaultValue: p.defaultValue,
|
|
672
670
|
}));
|
|
673
671
|
// Extract @ui template path from class-level JSDoc
|
|
674
|
-
const classJsdocMatch = source.match(/\/\*\*[\s\S]*?\*\/\s*(?=export\s+default\s+class)/)
|
|
675
|
-
|
|
672
|
+
const classJsdocMatch = source.match(/\/\*\*[\s\S]*?\*\/\s*(?=export\s+default\s+class)/) ||
|
|
673
|
+
source.match(/^\/\*\*([\s\S]*?)\*\//);
|
|
676
674
|
if (classJsdocMatch) {
|
|
677
675
|
const uiMatch = classJsdocMatch[0].match(/@ui\s+([^\s*]+)/);
|
|
678
676
|
if (uiMatch) {
|
|
@@ -745,7 +743,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
745
743
|
linkedUi: linkedAsset?.id,
|
|
746
744
|
...(schema.isStatic ? { isStatic: true } : {}),
|
|
747
745
|
...(schema.webhook ? { webhook: schema.webhook } : {}),
|
|
748
|
-
...(schema.scheduled || schema.cron
|
|
746
|
+
...(schema.scheduled || schema.cron
|
|
747
|
+
? { scheduled: schema.scheduled || schema.cron }
|
|
748
|
+
: {}),
|
|
749
749
|
...(schema.locked ? { locked: schema.locked } : {}),
|
|
750
750
|
};
|
|
751
751
|
});
|
|
@@ -819,7 +819,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
819
819
|
promptCount,
|
|
820
820
|
installSource,
|
|
821
821
|
...(constructorParams.length > 0 && { requiredParams: constructorParams }),
|
|
822
|
-
...(mcp.injectedPhotons &&
|
|
822
|
+
...(mcp.injectedPhotons &&
|
|
823
|
+
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
823
824
|
};
|
|
824
825
|
}
|
|
825
826
|
catch (error) {
|
|
@@ -1014,8 +1015,12 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1014
1015
|
return null; // UI asset not found
|
|
1015
1016
|
}
|
|
1016
1017
|
};
|
|
1018
|
+
// Security: rate limiter for API endpoints
|
|
1019
|
+
const apiRateLimiter = new SimpleRateLimiter(30, 60_000);
|
|
1017
1020
|
// Create HTTP server
|
|
1018
1021
|
const server = http.createServer(async (req, res) => {
|
|
1022
|
+
// Security: set standard security headers on all responses
|
|
1023
|
+
setSecurityHeaders(res);
|
|
1019
1024
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
1020
1025
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1021
1026
|
// MCP Streamable HTTP Transport (standard MCP clients like Claude Desktop)
|
|
@@ -1133,17 +1138,18 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1133
1138
|
root = path.resolve(workdirEnv);
|
|
1134
1139
|
}
|
|
1135
1140
|
}
|
|
1136
|
-
|
|
1141
|
+
// Security: default browse root to workingDir if not specified
|
|
1142
|
+
if (!root) {
|
|
1143
|
+
root = workingDir;
|
|
1144
|
+
}
|
|
1145
|
+
const dirPath = url.searchParams.get('path') || root;
|
|
1137
1146
|
try {
|
|
1138
1147
|
const resolved = path.resolve(dirPath);
|
|
1139
|
-
//
|
|
1140
|
-
if (root) {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
res.end(JSON.stringify({ error: 'Access denied: outside allowed directory' }));
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1148
|
+
// Security: always enforce path boundary using isPathWithin
|
|
1149
|
+
if (!isPathWithin(resolved, root)) {
|
|
1150
|
+
res.writeHead(403);
|
|
1151
|
+
res.end(JSON.stringify({ error: 'Access denied: outside allowed directory' }));
|
|
1152
|
+
return;
|
|
1147
1153
|
}
|
|
1148
1154
|
const stat = await fs.stat(resolved);
|
|
1149
1155
|
if (!stat.isDirectory()) {
|
|
@@ -1187,6 +1193,12 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1187
1193
|
return;
|
|
1188
1194
|
}
|
|
1189
1195
|
const resolved = path.resolve(filePath);
|
|
1196
|
+
// Security: prevent path traversal — file must be within working directory
|
|
1197
|
+
if (!isPathWithin(resolved, workingDir)) {
|
|
1198
|
+
res.writeHead(403);
|
|
1199
|
+
res.end('Access denied: outside allowed directory');
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1190
1202
|
try {
|
|
1191
1203
|
const fileStat = await fs.stat(resolved);
|
|
1192
1204
|
if (!fileStat.isFile()) {
|
|
@@ -1367,9 +1379,19 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1367
1379
|
}
|
|
1368
1380
|
// Resolve template path relative to photon's directory
|
|
1369
1381
|
const photonDir = path.dirname(photon.path);
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1382
|
+
// Security: reject absolute template paths — must be relative to photon dir
|
|
1383
|
+
if (path.isAbsolute(templateFile)) {
|
|
1384
|
+
res.writeHead(403);
|
|
1385
|
+
res.end(JSON.stringify({ error: 'Absolute template paths are not allowed' }));
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const fullTemplatePath = path.join(photonDir, templateFile);
|
|
1389
|
+
// Security: validate resolved path is within photon directory
|
|
1390
|
+
if (!isPathWithin(fullTemplatePath, photonDir)) {
|
|
1391
|
+
res.writeHead(403);
|
|
1392
|
+
res.end(JSON.stringify({ error: 'Template path traversal detected' }));
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1373
1395
|
try {
|
|
1374
1396
|
const templateContent = await fs.readFile(fullTemplatePath, 'utf-8');
|
|
1375
1397
|
res.setHeader('Content-Type', 'text/html');
|
|
@@ -1608,38 +1630,49 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1608
1630
|
}
|
|
1609
1631
|
// Invoke API: Direct HTTP endpoint for method invocation (used by PWA)
|
|
1610
1632
|
if (url.pathname === '/api/invoke' && req.method === 'POST') {
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
}
|
|
1632
|
-
const result = await mcp.instance[method](args || {});
|
|
1633
|
-
res.setHeader('Content-Type', 'application/json');
|
|
1634
|
-
res.writeHead(200);
|
|
1635
|
-
res.end(JSON.stringify({ result }));
|
|
1633
|
+
// Security: only allow local requests
|
|
1634
|
+
if (!isLocalRequest(req)) {
|
|
1635
|
+
res.writeHead(403);
|
|
1636
|
+
res.end(JSON.stringify({ error: 'Forbidden: non-local request' }));
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
// Security: rate limiting
|
|
1640
|
+
const clientKey = req.socket?.remoteAddress || 'unknown';
|
|
1641
|
+
if (!apiRateLimiter.isAllowed(clientKey)) {
|
|
1642
|
+
res.writeHead(429);
|
|
1643
|
+
res.end(JSON.stringify({ error: 'Too many requests' }));
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
const body = await readBody(req);
|
|
1648
|
+
const { photon: photonName, method, args } = JSON.parse(body);
|
|
1649
|
+
if (!photonName || !method) {
|
|
1650
|
+
res.writeHead(400);
|
|
1651
|
+
res.end(JSON.stringify({ error: 'Missing photon or method' }));
|
|
1652
|
+
return;
|
|
1636
1653
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
res.writeHead(
|
|
1640
|
-
res.end(JSON.stringify({ error:
|
|
1654
|
+
const mcp = photonMCPs.get(photonName);
|
|
1655
|
+
if (!mcp || !mcp.instance) {
|
|
1656
|
+
res.writeHead(404);
|
|
1657
|
+
res.end(JSON.stringify({ error: `Photon not found: ${photonName}` }));
|
|
1658
|
+
return;
|
|
1641
1659
|
}
|
|
1642
|
-
|
|
1660
|
+
if (typeof mcp.instance[method] !== 'function') {
|
|
1661
|
+
res.writeHead(404);
|
|
1662
|
+
res.end(JSON.stringify({ error: `Method not found: ${method}` }));
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const result = await mcp.instance[method](args || {});
|
|
1666
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1667
|
+
res.writeHead(200);
|
|
1668
|
+
res.end(JSON.stringify({ result }));
|
|
1669
|
+
}
|
|
1670
|
+
catch (err) {
|
|
1671
|
+
const status = err.message?.includes('too large') ? 413 : 500;
|
|
1672
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1673
|
+
res.writeHead(status);
|
|
1674
|
+
res.end(JSON.stringify({ error: err.message || String(err) }));
|
|
1675
|
+
}
|
|
1643
1676
|
return;
|
|
1644
1677
|
}
|
|
1645
1678
|
// Platform Bridge API: Generate platform compatibility script
|
|
@@ -2354,7 +2387,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2354
2387
|
icon: reloadClassMeta.icon,
|
|
2355
2388
|
internal: reloadClassMeta.internal,
|
|
2356
2389
|
...(reloadConstructorParams.length > 0 && { requiredParams: reloadConstructorParams }),
|
|
2357
|
-
...(mcp.injectedPhotons &&
|
|
2390
|
+
...(mcp.injectedPhotons &&
|
|
2391
|
+
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
2358
2392
|
};
|
|
2359
2393
|
if (isNewPhoton) {
|
|
2360
2394
|
photons.push(reloadedPhoton);
|
|
@@ -2499,7 +2533,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2499
2533
|
process.exit(1);
|
|
2500
2534
|
}
|
|
2501
2535
|
});
|
|
2502
|
-
|
|
2536
|
+
// Security: bind to localhost by default, configurable via BEAM_BIND_ADDRESS
|
|
2537
|
+
const bindAddress = process.env.BEAM_BIND_ADDRESS || '127.0.0.1';
|
|
2538
|
+
server.listen(currentPort, bindAddress, () => {
|
|
2503
2539
|
process.env.BEAM_PORT = String(currentPort);
|
|
2504
2540
|
const url = `http://localhost:${currentPort}`;
|
|
2505
2541
|
console.log(`\n⚡ Photon Beam → ${url} (loading photons...)\n`);
|
|
@@ -2529,9 +2565,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2529
2565
|
const photonStatus = unconfiguredCount > 0
|
|
2530
2566
|
? `${configuredCount} ready, ${unconfiguredCount} need setup`
|
|
2531
2567
|
: `${configuredCount} photon${configuredCount !== 1 ? 's' : ''} ready`;
|
|
2532
|
-
const mcpStatus = externalMCPList.length > 0
|
|
2533
|
-
? `, ${connectedMCPs}/${externalMCPList.length} MCPs`
|
|
2534
|
-
: '';
|
|
2568
|
+
const mcpStatus = externalMCPList.length > 0 ? `, ${connectedMCPs}/${externalMCPList.length} MCPs` : '';
|
|
2535
2569
|
console.log(`⚡ Photon Beam ready (${photonStatus}${mcpStatus})`);
|
|
2536
2570
|
// Notify connected clients that photon list is now available
|
|
2537
2571
|
broadcastPhotonChange();
|
|
@@ -2673,7 +2707,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2673
2707
|
externalMCPSDKClients.delete(name);
|
|
2674
2708
|
}
|
|
2675
2709
|
}
|
|
2676
|
-
catch {
|
|
2710
|
+
catch {
|
|
2711
|
+
/* ignore */
|
|
2712
|
+
}
|
|
2677
2713
|
externalMCPClients.delete(name);
|
|
2678
2714
|
logger.info(`🔌 Removed external MCP: ${name}`);
|
|
2679
2715
|
}
|
|
@@ -2701,7 +2737,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2701
2737
|
externalMCPSDKClients.delete(name);
|
|
2702
2738
|
}
|
|
2703
2739
|
}
|
|
2704
|
-
catch {
|
|
2740
|
+
catch {
|
|
2741
|
+
/* ignore */
|
|
2742
|
+
}
|
|
2705
2743
|
externalMCPClients.delete(name);
|
|
2706
2744
|
externalMCPs.splice(idx, 1);
|
|
2707
2745
|
}
|
|
@@ -2812,7 +2850,8 @@ async function configurePhotonViaMCP(photonName, config, photons, photonMCPs, lo
|
|
|
2812
2850
|
isApp,
|
|
2813
2851
|
appEntry: mainMethod,
|
|
2814
2852
|
assets: mcp.assets,
|
|
2815
|
-
...(mcp.injectedPhotons &&
|
|
2853
|
+
...(mcp.injectedPhotons &&
|
|
2854
|
+
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
2816
2855
|
};
|
|
2817
2856
|
photons[photonIndex] = configuredPhoton;
|
|
2818
2857
|
logger.info(`✅ ${photonName} configured via MCP`);
|
|
@@ -2908,7 +2947,8 @@ async function reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, saved
|
|
|
2908
2947
|
description: reloadClassMeta.description,
|
|
2909
2948
|
icon: reloadClassMeta.icon,
|
|
2910
2949
|
internal: reloadClassMeta.internal,
|
|
2911
|
-
...(mcp.injectedPhotons &&
|
|
2950
|
+
...(mcp.injectedPhotons &&
|
|
2951
|
+
mcp.injectedPhotons.length > 0 && { injectedPhotons: mcp.injectedPhotons }),
|
|
2912
2952
|
};
|
|
2913
2953
|
photons[photonIndex] = reloadedPhoton;
|
|
2914
2954
|
logger.info(`🔄 ${photonName} reloaded via MCP`);
|
|
@@ -3000,10 +3040,7 @@ async function generatePhotonHelpMarkdown(photonName, photons) {
|
|
|
3000
3040
|
const mdPath = path.join(sourceDir, `${photonName}.md`);
|
|
3001
3041
|
// Check if .md file already exists and is newer than the photon source
|
|
3002
3042
|
try {
|
|
3003
|
-
const [mdStat, srcStat] = await Promise.all([
|
|
3004
|
-
fs.stat(mdPath),
|
|
3005
|
-
fs.stat(photon.path),
|
|
3006
|
-
]);
|
|
3043
|
+
const [mdStat, srcStat] = await Promise.all([fs.stat(mdPath), fs.stat(photon.path)]);
|
|
3007
3044
|
if (mdStat.mtimeMs >= srcStat.mtimeMs) {
|
|
3008
3045
|
const existing = await fs.readFile(mdPath, 'utf-8');
|
|
3009
3046
|
if (existing.trim()) {
|