1use super::types::*;
8
9#[derive(Debug, Clone)]
13pub struct ContentConfig {
14 pub base_path: String,
15 pub name: String,
16 pub version: String,
17 pub description: String,
18 pub homepage: Option<String>,
19}
20
21impl ContentConfig {
22 pub fn new(base_path: &str) -> Self {
23 Self {
24 base_path: base_path.to_string(),
25 name: "cedros-data".to_string(),
26 version: env!("CARGO_PKG_VERSION").to_string(),
27 description: "Multi-site Postgres-backed content and custom data storage API"
28 .to_string(),
29 homepage: None,
30 }
31 }
32
33 pub fn path(&self, endpoint: &str) -> String {
34 format!("{}{}", self.base_path, endpoint)
35 }
36}
37
38impl Default for ContentConfig {
39 fn default() -> Self {
40 Self::new("")
41 }
42}
43
44pub fn get_skill_references(config: &ContentConfig) -> Vec<SkillReference> {
49 vec![
50 SkillReference {
51 id: "data".to_string(),
52 name: "Data".to_string(),
53 path: config.path("/skills/data.md"),
54 description: "Entry upsert, query, and collection management".to_string(),
55 requires_auth: Some(true),
56 requires_admin: None,
57 },
58 SkillReference {
59 id: "admin".to_string(),
60 name: "Admin".to_string(),
61 path: config.path("/skills/admin.md"),
62 description: "Site bootstrap, collection admin, page management".to_string(),
63 requires_auth: Some(true),
64 requires_admin: Some(true),
65 },
66 SkillReference {
67 id: "schema".to_string(),
68 name: "Schema".to_string(),
69 path: config.path("/skills/schema.md"),
70 description: "Custom schemas, contract verification, import/export".to_string(),
71 requires_auth: Some(true),
72 requires_admin: None,
73 },
74 SkillReference {
75 id: "storage".to_string(),
76 name: "Storage".to_string(),
77 path: config.path("/skills/storage.md"),
78 description: "Media upload, asset management, S3-compatible storage".to_string(),
79 requires_auth: Some(true),
80 requires_admin: Some(true),
81 },
82 SkillReference {
83 id: "site".to_string(),
84 name: "Site".to_string(),
85 path: config.path("/skills/site.md"),
86 description: "Site registration, migration, public config".to_string(),
87 requires_auth: Some(true),
88 requires_admin: None,
89 },
90 ]
91}
92
93pub fn get_skill_capabilities() -> SkillCapabilities {
94 SkillCapabilities {
95 multi_site: true,
96 collections: true,
97 custom_schema: true,
98 typed_tables: true,
99 contracts: true,
100 import_export: true,
101 default_pages: true,
102 media_storage: true,
103 metered_reads: true,
104 }
105}
106
107pub fn get_skill_auth() -> SkillAuth {
108 SkillAuth {
109 methods: vec![
110 "bearer-token".to_string(),
111 "x-cedros-org-id".to_string(),
112 ],
113 recommended: "bearer-token".to_string(),
114 header: "Authorization".to_string(),
115 }
116}
117
118pub fn get_rate_limits() -> RateLimits {
119 RateLimits {
120 api_endpoints: "100 req/min per key".to_string(),
121 admin_endpoints: "30 req/min per key".to_string(),
122 }
123}
124
125pub fn get_skill_metadata(config: &ContentConfig) -> SkillMetadata {
126 SkillMetadata {
127 name: config.name.clone(),
128 version: config.version.clone(),
129 description: config.description.clone(),
130 homepage: config.homepage.clone(),
131 api_base: config.base_path.clone(),
132 category: "data-storage".to_string(),
133 capabilities: get_skill_capabilities(),
134 skills: get_skill_references(config),
135 authentication: get_skill_auth(),
136 rate_limits: get_rate_limits(),
137 downloadable_bundles: Some(DownloadableBundles {
138 claude_code: config.path("/.well-known/skills.zip"),
139 codex: config.path("/.well-known/skills.zip"),
140 }),
141 }
142}
143
144pub fn get_discovery_index(config: &ContentConfig) -> AiDiscoveryIndex {
149 AiDiscoveryIndex {
150 version: "1.0.0".to_string(),
151 name: config.name.clone(),
152 description: config.description.clone(),
153 endpoints: DiscoveryEndpoints {
154 llms_txt: config.path("/llms.txt"),
155 llms_full_txt: config.path("/llms-full.txt"),
156 llms_admin_txt: Some(config.path("/llms-admin.txt")),
157 skill_index_markdown: config.path("/skill.md"),
158 skill_index_json: config.path("/skill.json"),
159 agent_guide: config.path("/agent.md"),
160 openapi: config.path("/openapi.json"),
161 a2a_agent_card: config.path("/.well-known/agent.json"),
162 ai_plugin: config.path("/.well-known/ai-plugin.json"),
163 mcp: config.path("/.well-known/mcp"),
164 health: config.path("/heartbeat.json"),
165 skills_bundle: Some(config.path("/.well-known/skills.zip")),
166 },
167 skills: Some(
168 get_skill_references(config)
169 .into_iter()
170 .map(|s| SkillPointer {
171 id: s.id,
172 name: s.name,
173 path: s.path,
174 })
175 .collect(),
176 ),
177 }
178}
179
180pub fn generate_ai_txt(config: &ContentConfig) -> String {
185 let base = &config.base_path;
186 format!(
187 r#"# AI Access Policy
188# Permissions for AI crawlers and agents.
189# See: https://ai-txt.org
190
191User-agent: *
192Allow: /
193
194AI-Discovery: {base}/.well-known/ai-discovery.json
195
196LLMs-Txt: {base}/llms.txt
197LLMs-Full: {base}/llms-full.txt
198OpenAPI: {base}/openapi.json
199Skills: {base}/skill.json
200Agent-Guide: {base}/agent.md
201
202# Authentication
203# Requests require a Bearer token or x-cedros-org-id header.
204# See: {base}/agent.md
205
206# Rate Limits
207# API endpoints: 100 req/min per key
208# Admin endpoints: 30 req/min per key
209"#,
210 base = base
211 )
212}
213
214pub fn generate_llms_txt(config: &ContentConfig) -> String {
215 let base = &config.base_path;
216 format!(
217 r#"# Cedros Data
218
219> Multi-site Postgres-backed content and custom data storage API. Supports JSONB and typed-table collections, custom schemas, contract verification, import/export, default page templates, media storage (S3), and metered reads.
220
221Cedros Data provides structured content storage with first-class support for multi-site architectures. Each site has isolated collections, schemas, and page templates.
222
223## Quick Start
224
2251. Register a site: `POST {base}/site` with `{{"display_name": "My Site"}}`
2262. Create a collection: `POST {base}/collections` with `{{"collection_name": "articles", "mode": "jsonb"}}`
2273. Upsert entries: `POST {base}/entries/upsert` with `{{"collection_name": "articles", "entry_key": "hello", "payload": {{}}}}`
2284. Query entries: `POST {base}/entries/query` with `{{"collection_name": "articles"}}`
229
230## Docs
231
232- [{base}/agent.md]({base}/agent.md): Agent integration guide
233- [{base}/llms-full.txt]({base}/llms-full.txt): Complete API documentation
234- [{base}/llms-admin.txt]({base}/llms-admin.txt): Admin operations reference
235
236## Skills
237
238- [{base}/skills/data.md]({base}/skills/data.md): Entry upsert, query, collections
239- [{base}/skills/admin.md]({base}/skills/admin.md): Site bootstrap, collection admin, pages
240- [{base}/skills/schema.md]({base}/skills/schema.md): Custom schemas, contracts, import/export
241- [{base}/skills/storage.md]({base}/skills/storage.md): Media upload, asset management
242- [{base}/skills/site.md]({base}/skills/site.md): Site registration, migration, config
243
244## API
245
246- [{base}/openapi.json]({base}/openapi.json): Full OpenAPI 3.0 specification
247- [{base}/skill.json]({base}/skill.json): Machine-readable skill metadata
248
249## Optional
250
251- [{base}/heartbeat.json]({base}/heartbeat.json): Health check endpoint
252- [{base}/.well-known/ai-discovery.json]({base}/.well-known/ai-discovery.json): Discovery index
253- [{base}/.well-known/agent.json]({base}/.well-known/agent.json): Google A2A Agent Card
254- [{base}/.well-known/mcp]({base}/.well-known/mcp): MCP server discovery
255"#,
256 base = base
257 )
258}
259
260pub fn generate_skill_md(config: &ContentConfig) -> String {
261 let base = &config.base_path;
262 let skills = get_skill_references(config);
263
264 let skills_yaml: Vec<String> = skills
265 .iter()
266 .map(|s| {
267 let mut yaml = format!(
268 " - id: {}\n path: {}\n requiresAuth: {}",
269 s.id,
270 s.path,
271 s.requires_auth.unwrap_or(false)
272 );
273 if let Some(true) = s.requires_admin {
274 yaml.push_str("\n requiresAdmin: true");
275 }
276 yaml
277 })
278 .collect();
279
280 let skills_table: Vec<String> = skills
281 .iter()
282 .map(|s| {
283 let auth = if s.requires_admin == Some(true) {
284 "Yes (Admin)"
285 } else if s.requires_auth == Some(true) {
286 "Yes"
287 } else {
288 "No"
289 };
290 format!(
291 "| [{}]({}) | {} | {} |",
292 s.name, s.path, s.description, auth
293 )
294 })
295 .collect();
296
297 format!(
298 r#"---
299name: cedros-data
300version: "{version}"
301description: Multi-site Postgres-backed content and custom data storage API
302category: data-storage
303apiBase: "{base}"
304capabilities:
305 multiSite: true
306 collections: true
307 customSchema: true
308 typedTables: true
309 contracts: true
310 importExport: true
311 defaultPages: true
312 mediaStorage: true
313 meteredReads: true
314authentication:
315 methods: [bearer-token, x-cedros-org-id]
316 recommended: bearer-token
317 header: "Authorization: Bearer <token>"
318rateLimits:
319 api: "100 req/min per key"
320 admin: "30 req/min per key"
321skills:
322{skills_yaml}
323---
324
325# Cedros Data Skills
326
327Multi-site Postgres-backed content and custom data storage API. Supports JSONB and typed-table collections, custom schemas, contract verification, import/export, default page templates, media storage (S3), and metered reads.
328
329## Available Skills
330
331| Skill | Description | Auth Required |
332|-------|-------------|---------------|
333{skills_table}
334
335## Quick Start
336
3371. Register a site: `POST {base}/site`
3382. Create a collection: `POST {base}/collections`
3393. Upsert entries: `POST {base}/entries/upsert`
3404. Query entries: `POST {base}/entries/query`
341
342## Discovery Endpoints
343
344| Endpoint | Format | Purpose |
345|----------|--------|---------|
346| {base}/llms.txt | text | Brief API summary |
347| {base}/llms-full.txt | text | Complete documentation |
348| {base}/llms-admin.txt | text | Admin operations |
349| {base}/skill.json | JSON | Machine-readable skill metadata |
350| {base}/agent.md | markdown | Integration guide |
351| {base}/openapi.json | JSON | Full OpenAPI specification |
352
353## Authentication
354
355All write operations require a Bearer token via the `Authorization` header. Multi-site operations use the `x-cedros-org-id` header for site scoping.
356
357## Error Format
358
359```json
360{{
361 "error": "Human-readable description"
362}}
363```
364"#,
365 version = config.version,
366 base = base,
367 skills_yaml = skills_yaml.join("\n"),
368 skills_table = skills_table.join("\n"),
369 )
370}
371
372pub fn get_ai_plugin_manifest(config: &ContentConfig) -> AiPluginManifest {
377 AiPluginManifest {
378 schema_version: "v1".to_string(),
379 name_for_human: "Cedros Data".to_string(),
380 name_for_model: "cedros_data".to_string(),
381 description_for_human: "Multi-site content and custom data storage API".to_string(),
382 description_for_model: format!(
383 "Cedros Data API for structured content storage. Supports JSONB and typed-table \
384 collections, custom schemas, contracts, import/export, and media storage. \
385 Create collections with POST {}/collections, upsert entries with POST {}/entries/upsert.",
386 config.base_path, config.base_path
387 ),
388 auth: AiPluginAuth {
389 auth_type: "bearer".to_string(),
390 instructions: Some(format!(
391 "Authenticate with a Bearer token. See {}/agent.md for details.",
392 config.base_path
393 )),
394 },
395 api: AiPluginApi {
396 api_type: "openapi".to_string(),
397 url: config.path("/openapi.json"),
398 },
399 }
400}
401
402pub fn get_agent_card(config: &ContentConfig) -> AgentCard {
403 let skills = get_skill_references(config);
404
405 AgentCard {
406 name: "Cedros Data".to_string(),
407 description: config.description.clone(),
408 url: config.base_path.clone(),
409 version: config.version.clone(),
410 capabilities: AgentCapabilities {
411 streaming: false,
412 push_notifications: false,
413 state_management: true,
414 },
415 authentication: AgentAuthentication {
416 schemes: vec![AuthScheme {
417 scheme: "bearer".to_string(),
418 description: "Bearer token authentication".to_string(),
419 instructions_url: Some(config.path("/agent.md")),
420 }],
421 },
422 skills: skills
423 .into_iter()
424 .map(|s| AgentSkill {
425 id: s.id.clone(),
426 name: s.name,
427 description: s.description,
428 input_modes: vec!["application/json".to_string()],
429 output_modes: vec!["application/json".to_string()],
430 documentation_url: Some(s.path),
431 openapi_tag: Some(s.id.to_uppercase()),
432 })
433 .collect(),
434 documentation_url: Some(config.path("/llms-full.txt")),
435 provider: Some(AgentProvider {
436 name: "Cedros".to_string(),
437 url: config.homepage.clone(),
438 }),
439 }
440}
441
442pub fn get_mcp_discovery(config: &ContentConfig) -> McpDiscovery {
443 McpDiscovery {
444 name: config.name.clone(),
445 version: config.version.clone(),
446 protocol_version: "2024-11-05".to_string(),
447 description: config.description.clone(),
448 capabilities: McpCapabilities {
449 tools: true,
450 resources: true,
451 prompts: false,
452 sampling: false,
453 },
454 tools: vec![
455 McpTool {
456 name: "upsert_entry".to_string(),
457 description: "Create or update a content entry in a collection".to_string(),
458 input_schema: serde_json::json!({
459 "type": "object",
460 "properties": {
461 "collection_name": {
462 "type": "string",
463 "description": "Target collection name"
464 },
465 "entry_key": {
466 "type": "string",
467 "description": "Unique entry key within the collection"
468 },
469 "payload": {
470 "type": "object",
471 "description": "Entry payload (JSON object)"
472 }
473 },
474 "required": ["collection_name", "entry_key", "payload"]
475 }),
476 },
477 McpTool {
478 name: "query_entries".to_string(),
479 description: "Query entries from a collection with optional filters".to_string(),
480 input_schema: serde_json::json!({
481 "type": "object",
482 "properties": {
483 "collection_name": {
484 "type": "string",
485 "description": "Collection to query"
486 },
487 "entry_keys": {
488 "type": "array",
489 "items": { "type": "string" },
490 "description": "Optional specific entry keys to fetch"
491 }
492 },
493 "required": ["collection_name"]
494 }),
495 },
496 McpTool {
497 name: "register_collection".to_string(),
498 description: "Create a new collection for storing entries".to_string(),
499 input_schema: serde_json::json!({
500 "type": "object",
501 "properties": {
502 "collection_name": {
503 "type": "string",
504 "description": "Name for the new collection"
505 },
506 "mode": {
507 "type": "string",
508 "enum": ["jsonb", "typed"],
509 "description": "Storage mode (jsonb or typed table)"
510 }
511 },
512 "required": ["collection_name", "mode"]
513 }),
514 },
515 ],
516 authentication: McpAuth {
517 required: true,
518 schemes: vec!["bearer".to_string()],
519 instructions: format!(
520 "Authenticate with a Bearer token. See {}/agent.md",
521 config.base_path
522 ),
523 },
524 }
525}