Skip to main content
Glama
plans.js15.6 kB
const express = require('express'); const Router = express.Router; const sqlite3 = require('sqlite3').verbose(); const { getDB } = require('../db/database.js'); const router = new Router(); async function getAll(db, sql, params = []) { if (!db) db = getDB(); if (!db) throw new Error('DB not initialized'); return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } async function getOne(db, sql, params = []) { if (!db) db = getDB(); if (!db) throw new Error('DB not initialized'); return new Promise((resolve, reject) => { db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(row); }); }); } async function runSql(db, sql, params = []) { if (!db) db = getDB(); if (!db) throw new Error('DB not initialized'); return new Promise((resolve, reject) => { db.run(sql, params, function(err) { if (err) reject(err); else resolve(this); }); }); } // POST / router.post('/', async (req, res, next) => { try { const db = req.db || getDB(); const { title, description, tags: inputTags } = req.body; if (!title || title.trim() === '' || !description || description.trim() === '') { return res.status(400).json({ error: 'Title and description are required and cannot be empty' }); } let tags = []; if (inputTags) { if (!Array.isArray(inputTags)) { return res.status(400).json({ error: 'Tags must be an array of strings' }); } tags = inputTags.filter(tag => typeof tag === 'string' && tag.trim() !== '').map(tag => tag.trim().toLowerCase()); const uniqueTags = [...new Set(tags)]; if (uniqueTags.length !== tags.length) { return res.status(400).json({ error: 'Tags must not contain duplicates' }); } if (uniqueTags.length > 10) { return res.status(400).json({ error: 'Maximum 10 tags allowed' }); } tags = uniqueTags; } const timestamp = new Date().toISOString(); const status = "proposed"; const changelog = "[]"; const createdAt = Date.now(); const result = await runSql(db, "INSERT INTO plans (title, description, status, changelog, timestamp, created_at, last_modified_by, last_modified_at, needs_review, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)", [title, description, status, changelog, timestamp, createdAt, 'agent', createdAt, JSON.stringify(tags)] ); const id = result.lastID; console.log(`POST /plans: Inserted ID ${id}, title: "${title}"`); res.status(201).json({ id, title, description, status, timestamp, tags }); } catch (err) { next(err); } }); // GET /:id router.get('/:id', async (req, res, next) => { try { const db = req.db || getDB(); const planId = parseInt(req.params.id); const plan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); if (!plan) { const err = new Error('Plan not found'); err.status = 404; throw err; } const responsePlan = { id: plan.id, title: plan.title, description: plan.description, status: plan.status, timestamp: plan.timestamp, created_at: plan.created_at, last_modified_at: plan.last_modified_at, last_modified_by: plan.last_modified_by, needs_review: plan.needs_review, changelog: JSON.parse(plan.changelog), tags: JSON.parse(plan.tags || '[]') }; res.status(200).json(responsePlan); } catch (err) { next(err); } }); // PATCH /:id router.patch('/:id', async (req, res, next) => { try { const db = req.db || getDB(); const { status, needs_review } = req.body; const validStatuses = ['proposed', 'in_progress', 'completed']; if (status && !validStatuses.includes(status)) { return res.status(400).json({ error: 'Invalid status. Must be one of: proposed, in_progress, completed' }); } const planId = parseInt(req.params.id); let doUpdate = false; let updateFields = []; let params = []; if (status !== undefined) { updateFields.push('status = ?'); params.push(status); doUpdate = true; } if (needs_review !== undefined) { const nrValue = needs_review ? 1 : 0; updateFields.push('needs_review = ?'); params.push(nrValue); doUpdate = true; } else if (status !== undefined) { updateFields.push('needs_review = ?'); params.push(0); } let updatedPlan; if (doUpdate) { const now = Date.now(); updateFields.push('last_modified_by = ?'); updateFields.push('last_modified_at = ?'); params.push('agent'); params.push(now); const sql = `UPDATE plans SET ${updateFields.join(', ')} WHERE id = ?`; params.push(planId); const result = await runSql(db, sql, params); if (result.changes === 0) { const err = new Error('Plan not found'); err.status = 404; throw err; } updatedPlan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); } else { const current = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); if (!current) { const err = new Error('Plan not found'); err.status = 404; throw err; } updatedPlan = current; } const responsePlan = { id: updatedPlan.id, title: updatedPlan.title, description: updatedPlan.description, status: updatedPlan.status, timestamp: updatedPlan.timestamp, created_at: updatedPlan.created_at, last_modified_at: updatedPlan.last_modified_at, last_modified_by: updatedPlan.last_modified_by, needs_review: updatedPlan.needs_review, changelog: JSON.parse(updatedPlan.changelog) }; if (needs_review !== undefined) { res.status(200).json(responsePlan); } else { res.status(200).json({ status: updatedPlan.status }); } } catch (err) { next(err); } }); // PUT /:id router.put('/:id', async (req, res, next) => { try { const db = req.db || getDB(); const { title, description, tags: inputTags } = req.body; let updateFields = []; let params = []; if (title !== undefined) { if (!title || title.trim() === '') { return res.status(400).json({ error: 'Title cannot be empty if provided' }); } updateFields.push('title = COALESCE(?, title)'); params.push(title.trim()); } if (description !== undefined) { if (!description || description.trim() === '') { return res.status(400).json({ error: 'Description cannot be empty if provided' }); } updateFields.push('description = COALESCE(?, description)'); params.push(description.trim()); } let tags = null; if (inputTags !== undefined) { if (inputTags === null) { tags = []; } else if (!Array.isArray(inputTags)) { return res.status(400).json({ error: 'Tags must be an array of strings or null' }); } else { const processedTags = inputTags.filter(tag => typeof tag === 'string' && tag.trim() !== '').map(tag => tag.trim().toLowerCase()); const uniqueTags = [...new Set(processedTags)]; if (uniqueTags.length !== processedTags.length) { return res.status(400).json({ error: 'Tags must not contain duplicates' }); } if (uniqueTags.length > 10) { return res.status(400).json({ error: 'Maximum 10 tags allowed' }); } tags = uniqueTags; } updateFields.push('tags = ?'); params.push(JSON.stringify(tags)); } if (updateFields.length === 0) { return res.status(400).json({ error: 'At least one field must be provided' }); } const now = Date.now(); const setClause = updateFields.join(', ') + ', last_modified_by = ?, last_modified_at = ?, needs_review = 1'; params.push('human'); params.push(now); const sql = `UPDATE plans SET ${setClause} WHERE id = ?`; params.push(parseInt(req.params.id)); const planId = parseInt(req.params.id); const result = await runSql(db, sql, params); if (result.changes === 0) { const err = new Error('Plan not found'); err.status = 404; throw err; } const updatedPlan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); const responsePlan = { id: updatedPlan.id, title: updatedPlan.title, description: updatedPlan.description, status: updatedPlan.status, timestamp: updatedPlan.timestamp, created_at: updatedPlan.created_at, last_modified_at: updatedPlan.last_modified_at, last_modified_by: updatedPlan.last_modified_by, needs_review: updatedPlan.needs_review, changelog: JSON.parse(updatedPlan.changelog), tags: JSON.parse(updatedPlan.tags || '[]') }; res.status(200).json(responsePlan); } catch (err) { next(err); } }); // PATCH /:id/changelog router.patch('/:id/changelog', async (req, res, next) => { try { const db = req.db || getDB(); const { change } = req.body; if (!change || change.trim() === '') { return res.status(400).json({ error: 'Change is required and cannot be empty' }); } const planId = parseInt(req.params.id); const plan = await getOne(db, "SELECT changelog FROM plans WHERE id = ?", [planId]); if (!plan) { const err = new Error('Plan not found'); err.status = 404; throw err; } let changelog = JSON.parse(plan.changelog || '[]'); const timestamp = Date.now(); changelog.push({ timestamp, change: change.trim() }); const now = Date.now(); await runSql(db, "UPDATE plans SET changelog = ?, last_modified_by = 'agent', last_modified_at = ?, needs_review = 0 WHERE id = ?", [JSON.stringify(changelog), now, planId]); const updatedPlan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); const responsePlan = { id: updatedPlan.id, title: updatedPlan.title, description: updatedPlan.description, status: updatedPlan.status, timestamp: updatedPlan.timestamp, created_at: updatedPlan.created_at, last_modified_at: updatedPlan.last_modified_at, last_modified_by: updatedPlan.last_modified_by, needs_review: updatedPlan.needs_review, changelog }; res.status(200).json(responsePlan); } catch (err) { next(err); } }); // GET / router.patch('/:id/tags', async (req, res, next) => { try { const db = req.db || getDB(); const { add, remove } = req.body; const planId = parseInt(req.params.id); if (!add && !remove) { return res.status(400).json({ error: 'At least one of "add" or "remove" must be provided as arrays' }); } const plan = await getOne(db, "SELECT tags FROM plans WHERE id = ?", [planId]); if (!plan) { const err = new Error('Plan not found'); err.status = 404; throw err; } let currentTags = JSON.parse(plan.tags || '[]'); let newTags = [...currentTags]; if (add) { if (!Array.isArray(add)) { return res.status(400).json({ error: '"add" must be an array of strings' }); } const addTags = add.filter(tag => typeof tag === 'string' && tag.trim() !== '').map(tag => tag.trim().toLowerCase()); const uniqueAdd = addTags.filter(tag => !newTags.includes(tag)); newTags = [...new Set([...newTags, ...uniqueAdd])]; } if (remove) { if (!Array.isArray(remove)) { return res.status(400).json({ error: '"remove" must be an array of strings' }); } const removeTags = remove.filter(tag => typeof tag === 'string' && tag.trim() !== '').map(tag => tag.trim().toLowerCase()); newTags = newTags.filter(tag => !removeTags.includes(tag)); } if (newTags.length > 10) { return res.status(400).json({ error: 'Maximum 10 tags allowed after operation' }); } const now = Date.now(); await runSql(db, "UPDATE plans SET tags = ?, last_modified_by = 'agent', last_modified_at = ? WHERE id = ?", [JSON.stringify(newTags), now, planId]); const updatedPlan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); const responsePlan = { id: updatedPlan.id, title: updatedPlan.title, description: updatedPlan.description, status: updatedPlan.status, timestamp: updatedPlan.timestamp, tags: newTags }; res.status(200).json(responsePlan); } catch (err) { next(err); } }); router.get('/', async (req, res, next) => { try { const db = req.db || getDB(); const validStatuses = ['proposed', 'in_progress', 'completed']; let whereClauses = []; let sqlParams = []; const since = Number(req.query.since); if (!isNaN(since)) { whereClauses.push("created_at >= ?"); sqlParams.push(since); } if (req.query.status && validStatuses.includes(req.query.status)) { whereClauses.push("status = ?"); sqlParams.push(req.query.status); } if (req.query.needs_review === 'true') { whereClauses.push("needs_review = 1"); } // Tags filtering if (req.query.tags) { const tagsValue = req.query.tags.toString().trim(); let mode = 'any'; let tagsList = []; if (tagsValue.startsWith('any:')) { tagsList = tagsValue.substring(4).split(',').map(t => t.trim().toLowerCase()).filter(t => t); } else if (tagsValue.startsWith('all:')) { mode = 'all'; tagsList = tagsValue.substring(4).split(',').map(t => t.trim().toLowerCase()).filter(t => t); } else { tagsList = tagsValue.split(',').map(t => t.trim().toLowerCase()).filter(t => t); } if (tagsList.length > 0) { const tagConditions = tagsList.map(tag => `tags LIKE '%"${tag}"%'`).join(mode === 'all' ? ' AND ' : ' OR '); whereClauses.push(`(${tagConditions})`); } } let sql = "SELECT * FROM plans"; if (whereClauses.length > 0) { sql += " WHERE " + whereClauses.join(" AND "); } sql += " ORDER BY created_at ASC"; let plans = await getAll(db, sql, sqlParams); plans = plans.map(p => ({ id: p.id, title: p.title, description: p.description, status: p.status, timestamp: p.timestamp, created_at: p.created_at, last_modified_at: p.last_modified_at, last_modified_by: p.last_modified_by, needs_review: p.needs_review, changelog: JSON.parse(p.changelog), tags: JSON.parse(p.tags || '[]') })); console.log(`GET /plans: Returning ${plans.length} plans`); res.status(200).json(plans); } catch (err) { next(err); } }); // GET /:id/thoughts router.get('/:id/thoughts', async (req, res, next) => { try { const db = req.db || getDB(); const planId = parseInt(req.params.id); if (isNaN(planId)) { return res.status(400).json({ error: 'Invalid plan ID' }); } const plan = await getOne(db, "SELECT * FROM plans WHERE id = ?", [planId]); if (!plan) { return res.status(200).json([]); } const thoughts = await getAll(db, "SELECT * FROM thoughts WHERE plan_id = ? ORDER BY timestamp ASC", [planId] ); const responseThoughts = thoughts.map(t => ({ id: t.id.toString(), content: t.content, timestamp: t.timestamp, plan_id: t.plan_id ? t.plan_id.toString() : undefined })); res.status(200).json(responseThoughts); } catch (err) { next(err); } }); module.exports = router;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/suttonwilliamd/tpc-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server