Add MCP self-referencing docs and /docs endpoint
- Enhanced /.well-known/mcp.json to extract hostname from request headers (SNI/Host) - Added self-referencing URLs that work with localhost, domains, and reverse proxies - Documented all 5 API endpoints (random, raw, stream, cameras, health) as MCP tools - Added /docs endpoint serving HTML documentation from skill.md - Added /docs/skill.md for raw markdown access - Added /docs/mcp.json as alias to /.well-known/mcp.json - Created skill.md with comprehensive API documentation - Markdown to HTML conversion happens at build time Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
40451b7d4c
commit
05aea9530b
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Camera TRNG API Skill Documentation
|
||||||
|
|
||||||
|
This document describes how to use the Camera TRNG (True Random Number Generator) API as an MCP-compatible service.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Camera TRNG server provides quantum-based random number generation using thermal noise from a covered camera sensor. The API is accessible via HTTP and self-documents its capabilities through the `.well-known/mcp.json` endpoint.
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
The API can be discovered by fetching the MCP endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8787/.well-known/mcp.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes self-referencing URLs that use the request's hostname (from SNI/HTTP Host header), making it work correctly whether accessed via `localhost`, a domain name, or behind a reverse proxy.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Get Random Bytes (`/random`)
|
||||||
|
|
||||||
|
Generate cryptographically secure random bytes from camera sensor entropy.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `bytes` (integer, default: 32, max: 1048576): Number of random bytes to generate
|
||||||
|
- `hex` (boolean, default: false): Return bytes as hexadecimal string instead of binary
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Get 32 bytes as binary
|
||||||
|
curl "http://localhost:8787/random?bytes=32" -o random.bin
|
||||||
|
|
||||||
|
# Get 64 bytes as hex string
|
||||||
|
curl "http://localhost:8787/random?bytes=64&hex=true"
|
||||||
|
|
||||||
|
# Get 1KB of random data
|
||||||
|
curl "http://localhost:8787/random?bytes=1024" -o random.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get Raw Data (`/raw`)
|
||||||
|
|
||||||
|
Get raw LSB (least significant bit) data from camera sensor without cryptographic extraction. Useful for testing or custom post-processing.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `bytes` (integer, default: 65536, max: 1048576): Number of raw bytes to extract
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8787/raw?bytes=1024" -o raw.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Stream Random Bytes (`/stream`)
|
||||||
|
|
||||||
|
Stream continuous random bytes using Server-Sent Events (SSE) format. Useful for high-throughput applications.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `bytes` (integer, optional): Total bytes to stream (omit for unlimited)
|
||||||
|
- `hex` (boolean, default: false): Stream as hexadecimal strings
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Stream unlimited random bytes
|
||||||
|
curl -N "http://localhost:8787/stream"
|
||||||
|
|
||||||
|
# Stream exactly 1MB
|
||||||
|
curl -N "http://localhost:8787/stream?bytes=1048576"
|
||||||
|
|
||||||
|
# Stream as hex
|
||||||
|
curl -N "http://localhost:8787/stream?hex=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. List Cameras (`/cameras`)
|
||||||
|
|
||||||
|
List available camera devices on the system.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8787/cameras"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cameras": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"human_name": "FaceTime HD Camera",
|
||||||
|
"description": "Built-in camera",
|
||||||
|
"misc": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Health Check (`/health`)
|
||||||
|
|
||||||
|
Check if the TRNG server is running.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8787/health"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** `ok`
|
||||||
|
|
||||||
|
## MCP Integration
|
||||||
|
|
||||||
|
The API implements the Model Context Protocol (MCP) specification, allowing AI assistants and other MCP clients to discover and use the TRNG service automatically.
|
||||||
|
|
||||||
|
### MCP Tools
|
||||||
|
|
||||||
|
The following tools are exposed via MCP:
|
||||||
|
|
||||||
|
1. **get-random**: Generate random bytes (cryptographically secure)
|
||||||
|
2. **get-raw**: Extract raw sensor data
|
||||||
|
3. **get-stream**: Stream random bytes continuously
|
||||||
|
4. **list-cameras**: Discover available cameras
|
||||||
|
5. **health-check**: Verify server status
|
||||||
|
|
||||||
|
### Self-Referencing URLs
|
||||||
|
|
||||||
|
The MCP endpoint (`/.well-known/mcp.json`) automatically detects the request's hostname from:
|
||||||
|
- HTTP `Host` header
|
||||||
|
- `X-Forwarded-Proto` header (for reverse proxy scenarios)
|
||||||
|
|
||||||
|
This ensures URLs in the MCP response always reference the correct origin, whether accessed via:
|
||||||
|
- `localhost:8787`
|
||||||
|
- `trng.example.com`
|
||||||
|
- Behind a reverse proxy with forwarded headers
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Python
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Get 32 random bytes
|
||||||
|
response = requests.get("http://localhost:8787/random?bytes=32")
|
||||||
|
random_bytes = response.content
|
||||||
|
|
||||||
|
# Get hex string
|
||||||
|
response = requests.get("http://localhost:8787/random?bytes=64&hex=true")
|
||||||
|
hex_string = response.text
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/Node.js
|
||||||
|
```javascript
|
||||||
|
// Get random bytes
|
||||||
|
const response = await fetch("http://localhost:8787/random?bytes=32");
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
// Stream random data
|
||||||
|
const stream = await fetch("http://localhost:8787/stream?bytes=1024");
|
||||||
|
const reader = stream.body.getReader();
|
||||||
|
// ... read chunks
|
||||||
|
```
|
||||||
|
|
||||||
|
### curl
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
curl "http://localhost:8787/random?bytes=32&hex=true"
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
curl "http://localhost:8787/random?bytes=1024" -o random.bin
|
||||||
|
|
||||||
|
# Stream
|
||||||
|
curl -N "http://localhost:8787/stream"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Maximum 1MB per request (`/random` and `/raw`)
|
||||||
|
- Maximum 4 concurrent requests
|
||||||
|
- Requires a camera device with covered lens
|
||||||
|
- Performance depends on camera frame rate
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- No authentication required (intended for local/trusted networks)
|
||||||
|
- Random data is cryptographically secure when using `/random` endpoint
|
||||||
|
- Raw endpoint (`/raw`) provides unprocessed sensor data
|
||||||
|
- Consider rate limiting in production deployments
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- `TECHNICAL.md` - Technical implementation details
|
||||||
|
- `README.md` - Project overview and setup instructions
|
||||||
245
src/main.rs
245
src/main.rs
|
|
@ -5,7 +5,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::Query,
|
extract::Query,
|
||||||
http::{header, StatusCode},
|
http::{header, HeaderMap, StatusCode, Uri},
|
||||||
response::{Html, IntoResponse, Response, Json},
|
response::{Html, IntoResponse, Response, Json},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
|
|
@ -77,7 +77,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.route("/raw", get(get_raw))
|
.route("/raw", get(get_raw))
|
||||||
.route("/stream", get(get_stream))
|
.route("/stream", get(get_stream))
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.route("/.well-known/mcp.json", get(mcp_wellknown));
|
.route("/.well-known/mcp.json", get(mcp_wellknown))
|
||||||
|
.route("/docs", get(get_docs))
|
||||||
|
.route("/docs/skill.md", get(get_skill_md))
|
||||||
|
.route("/docs/mcp.json", get(mcp_wellknown));
|
||||||
|
|
||||||
let addr = format!("0.0.0.0:{}", port);
|
let addr = format!("0.0.0.0:{}", port);
|
||||||
println!("Camera QRNG (LavaRnd-style) on http://{}", addr);
|
println!("Camera QRNG (LavaRnd-style) on http://{}", addr);
|
||||||
|
|
@ -89,6 +92,156 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
|
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
|
||||||
async fn health() -> &'static str { "ok" }
|
async fn health() -> &'static str { "ok" }
|
||||||
|
|
||||||
|
async fn get_docs() -> Html<String> {
|
||||||
|
// Convert markdown to HTML (simple version)
|
||||||
|
let html = format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Camera TRNG API Documentation</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; line-height: 1.6; }}
|
||||||
|
h1 {{ border-bottom: 2px solid #333; padding-bottom: 10px; }}
|
||||||
|
h2 {{ margin-top: 30px; border-bottom: 1px solid #ccc; padding-bottom: 5px; }}
|
||||||
|
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: "Monaco", "Courier New", monospace; }}
|
||||||
|
pre {{ background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }}
|
||||||
|
pre code {{ background: none; padding: 0; }}
|
||||||
|
a {{ color: #0066cc; text-decoration: none; }}
|
||||||
|
a:hover {{ text-decoration: underline; }}
|
||||||
|
.endpoint {{ background: #e8f4f8; padding: 15px; margin: 15px 0; border-left: 4px solid #0066cc; border-radius: 3px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Camera TRNG API Documentation</h1>
|
||||||
|
<p><a href="/docs/skill.md">View as Markdown</a> | <a href="/docs/mcp.json">View MCP JSON</a> | <a href="/">Back to Home</a></p>
|
||||||
|
<hr>
|
||||||
|
{}
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
markdown_to_html(SKILL_MD)
|
||||||
|
);
|
||||||
|
Html(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_skill_md() -> Response {
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/markdown; charset=utf-8")
|
||||||
|
.body(Body::from(SKILL_MD))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn markdown_to_html(md: &str) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut html = String::new();
|
||||||
|
let mut in_code_block = false;
|
||||||
|
let mut code_content = String::new();
|
||||||
|
|
||||||
|
for line in md.lines() {
|
||||||
|
if line.starts_with("```") {
|
||||||
|
if in_code_block {
|
||||||
|
write!(html, "<pre><code>{}</code></pre>
|
||||||
|
", escape_html(&code_content)).unwrap();
|
||||||
|
code_content.clear();
|
||||||
|
in_code_block = false;
|
||||||
|
} else {
|
||||||
|
in_code_block = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_code_block {
|
||||||
|
code_content.push_str(line);
|
||||||
|
code_content.push_str("\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
html.push_str("<br>\n");
|
||||||
|
} else if trimmed.starts_with("### ") {
|
||||||
|
write!(html, "<h3>{}</h3>
|
||||||
|
", escape_html(&trimmed[4..])).unwrap();
|
||||||
|
} else if trimmed.starts_with("## ") {
|
||||||
|
write!(html, "<h2>{}</h2>
|
||||||
|
", escape_html(&trimmed[3..])).unwrap();
|
||||||
|
} else if trimmed.starts_with("# ") {
|
||||||
|
write!(html, "<h1>{}</h1>
|
||||||
|
", escape_html(&trimmed[2..])).unwrap();
|
||||||
|
} else {
|
||||||
|
// Simple inline processing
|
||||||
|
let mut processed = String::new();
|
||||||
|
let mut chars = trimmed.chars().peekable();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'`' => {
|
||||||
|
let mut code = String::new();
|
||||||
|
while let Some(&next) = chars.peek() {
|
||||||
|
if next == '`' {
|
||||||
|
chars.next();
|
||||||
|
write!(processed, "<code>{}</code>", escape_html(&code)).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
code.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'*' if chars.peek() == Some(&'*') => {
|
||||||
|
chars.next();
|
||||||
|
let mut bold = String::new();
|
||||||
|
while let Some(&next) = chars.peek() {
|
||||||
|
if next == '*' && chars.clone().nth(1) == Some('*') {
|
||||||
|
chars.next();
|
||||||
|
chars.next();
|
||||||
|
write!(processed, "<strong>{}</strong>", escape_html(&bold)).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bold.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'[' => {
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut url = String::new();
|
||||||
|
while let Some(&next) = chars.peek() {
|
||||||
|
if next == ']' {
|
||||||
|
chars.next();
|
||||||
|
if chars.peek() == Some(&'(') {
|
||||||
|
chars.next();
|
||||||
|
while let Some(&next) = chars.peek() {
|
||||||
|
if next == ')' {
|
||||||
|
chars.next();
|
||||||
|
write!(processed, "<a href=\"{}\">{}</a>", url, escape_html(&text)).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
url.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => processed.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(html, "<p>{}</p>
|
||||||
|
", processed).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_html(s: &str) -> String {
|
||||||
|
s.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async fn get_cameras() -> Response {
|
async fn get_cameras() -> Response {
|
||||||
match tokio::task::spawn_blocking(list_cameras).await {
|
match tokio::task::spawn_blocking(list_cameras).await {
|
||||||
Ok(Ok(cameras)) => Json(serde_json::json!({ "cameras": cameras })).into_response(),
|
Ok(Ok(cameras)) => Json(serde_json::json!({ "cameras": cameras })).into_response(),
|
||||||
|
|
@ -97,27 +250,98 @@ async fn get_cameras() -> Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mcp_wellknown() -> Json<serde_json::Value> {
|
async fn mcp_wellknown(headers: HeaderMap, _uri: Uri) -> Json<serde_json::Value> {
|
||||||
|
// Extract origin from request headers (Host header + scheme)
|
||||||
|
let host = headers
|
||||||
|
.get("host")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or("localhost:8787");
|
||||||
|
|
||||||
|
// Determine scheme from X-Forwarded-Proto or default to http
|
||||||
|
let scheme = headers
|
||||||
|
.get("x-forwarded-proto")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or("http");
|
||||||
|
|
||||||
|
let origin = format!("{}://{}", scheme, host);
|
||||||
|
|
||||||
|
// Build URL templates as strings first
|
||||||
|
let random_url = format!("{}/random?bytes={{bytes}}&hex={{hex}}", origin);
|
||||||
|
let raw_url = format!("{}/raw?bytes={{bytes}}", origin);
|
||||||
|
let stream_url = format!("{}/stream?bytes={{bytes}}&hex={{hex}}", origin);
|
||||||
|
let cameras_url = format!("{}/cameras", origin);
|
||||||
|
let health_url = format!("{}/health", origin);
|
||||||
|
let mcp_url = format!("{}/.well-known/mcp.json", origin);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"spec_version": "2026-01-21",
|
"spec_version": "2026-01-21",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"servers": [],
|
"servers": [],
|
||||||
"tools": [{
|
"tools": [
|
||||||
"name": "camera-qrng",
|
{
|
||||||
"description": "High-throughput quantum RNG using thermal noise from covered camera sensor",
|
"name": "get-random",
|
||||||
"url_template": "{origin}/random?bytes={bytes}&hex={hex}",
|
"description": "Get cryptographically secure random bytes from camera sensor entropy",
|
||||||
|
"url_template": random_url,
|
||||||
"capabilities": ["random-generation", "entropy-source", "quantum"],
|
"capabilities": ["random-generation", "entropy-source", "quantum"],
|
||||||
"auth": { "type": "none" },
|
"auth": { "type": "none" },
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"bytes": { "type": "integer", "default": 32, "max": 1048576 },
|
"bytes": { "type": "integer", "default": 32, "min": 1, "max": 1048576, "description": "Number of random bytes to generate (max 1MB)" },
|
||||||
"hex": { "type": "boolean", "default": false }
|
"hex": { "type": "boolean", "default": false, "description": "Return bytes as hexadecimal string instead of binary" }
|
||||||
}
|
}
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"name": "get-raw",
|
||||||
|
"description": "Get raw LSB (least significant bit) data from camera sensor without cryptographic extraction",
|
||||||
|
"url_template": raw_url,
|
||||||
|
"capabilities": ["entropy-source", "raw-data"],
|
||||||
|
"auth": { "type": "none" },
|
||||||
|
"parameters": {
|
||||||
|
"bytes": { "type": "integer", "default": 65536, "min": 1, "max": 1048576, "description": "Number of raw bytes to extract (max 1MB)" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get-stream",
|
||||||
|
"description": "Stream continuous random bytes (SSE format). Use ?bytes=N to limit total bytes, ?hex=true for hex output",
|
||||||
|
"url_template": stream_url,
|
||||||
|
"capabilities": ["random-generation", "entropy-source", "quantum", "streaming"],
|
||||||
|
"auth": { "type": "none" },
|
||||||
|
"parameters": {
|
||||||
|
"bytes": { "type": "integer", "optional": true, "description": "Total bytes to stream (omit for unlimited)" },
|
||||||
|
"hex": { "type": "boolean", "default": false, "description": "Stream as hexadecimal strings" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list-cameras",
|
||||||
|
"description": "List available camera devices",
|
||||||
|
"url_template": cameras_url,
|
||||||
|
"capabilities": ["device-discovery"],
|
||||||
|
"auth": { "type": "none" },
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "health-check",
|
||||||
|
"description": "Check if the TRNG server is running",
|
||||||
|
"url_template": health_url,
|
||||||
|
"capabilities": ["health"],
|
||||||
|
"auth": { "type": "none" },
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"uri": mcp_url,
|
||||||
|
"name": "MCP Documentation",
|
||||||
|
"description": "This MCP endpoint documentation",
|
||||||
|
"mimeType": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async fn get_random(Query(params): Query<RandomQuery>) -> Response {
|
async fn get_random(Query(params): Query<RandomQuery>) -> Response {
|
||||||
let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst);
|
let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst);
|
||||||
if current >= MAX_CONCURRENT {
|
if current >= MAX_CONCURRENT {
|
||||||
|
|
@ -222,3 +446,4 @@ async fn get_stream(Query(params): Query<StreamQuery>) -> Response {
|
||||||
|
|
||||||
|
|
||||||
const INDEX_HTML: &str = include_str!("index.html");
|
const INDEX_HTML: &str = include_str!("index.html");
|
||||||
|
const SKILL_MD: &str = include_str!("../skill.md");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue