logggai-mcp
Version:
Generic MCP server for Logggai (multi-IDE, Cursor wrapper, extensible)
1,045 lines (1,000 loc) • 43.6 kB
JavaScript
#!/usr/bin/env node
// ==== IMPORTS MCP/SDK ====
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { exec, spawn } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
import fs from "fs";
import path from "path";
import os from "os";
// ==============================================================
// --- Auth helpers for MCP ---
function isTokenExpired(token) {
if (!token) return true;
try {
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString('utf8'));
if (!payload.exp) return true;
const now = Math.floor(Date.now() / 1000);
return payload.exp < (now + 120); // 2 min margin
} catch {
return true;
}
}
// Verify MCP JWT token structure and claims
function verifyMcpToken(token) {
if (!token) {
throw new Error('No MCP token provided');
}
try {
// Decode JWT payload (basic validation)
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString('utf8'));
// Verify required MCP claims
if (!payload.aud || (payload.aud !== 'logggai-mcp' && payload.aud !== 'https://logggai.run/api/mcp')) {
throw new Error('Invalid audience: expected logggai-mcp or https://logggai.run/api/mcp');
}
if (!payload.sub) {
throw new Error('Missing subject (user ID) in token');
}
if (!payload.scopes || !Array.isArray(payload.scopes) || !payload.scopes.includes('agent')) {
throw new Error('Invalid scopes: expected ["agent"]');
}
if (payload.iss !== 'logggai') {
throw new Error('Invalid issuer: expected logggai');
}
// Check expiration
if (isTokenExpired(token)) {
throw new Error('Token has expired');
}
return payload;
} catch (error) {
if (error.message.includes('Invalid') || error.message.includes('Missing') || error.message.includes('expired')) {
throw error;
}
throw new Error('Invalid JWT token format');
}
}
// --- Audit logging helper ---
function logAudit({ tool, args, result, error }) {
// No-op in production: remove audit logs for npm package
}
// --- Daemon management helpers ---
function getPidFilePath() {
return path.join(process.cwd(), '.logggai-daemon.pid');
}
function getLogFilePath() {
return path.join(process.cwd(), '.logggai-daemon.log');
}
function getProjectConfigPath() {
return path.join(process.cwd(), '.logggai-project.json');
}
async function isDaemonRunning() {
const pidFile = getPidFilePath();
if (!fs.existsSync(pidFile)) {
return false;
}
try {
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
if (isNaN(pid)) return false;
// Check if process is still running
process.kill(pid, 0);
return true;
} catch (error) {
// Process doesn't exist, clean up stale PID file
try {
fs.unlinkSync(pidFile);
} catch {}
return false;
}
}
async function getLastLogLines(numLines = 10) {
const logFile = getLogFilePath();
if (!fs.existsSync(logFile)) {
return 'No log file found.';
}
try {
const { stdout } = await execAsync(`tail -n ${numLines} "${logFile}"`);
return stdout.trim();
} catch (error) {
return `Error reading log file: ${error.message}`;
}
}
// --- HTTP helper for SaaS endpoints ---
async function fetchSaas({ method = 'GET', url, body }) {
const token = process.env.API_KEY;
if (!token) {
throw new Error('No API key found. Please set API_KEY environment variable with a valid JWT token.');
}
// Verify MCP token structure and claims
try {
verifyMcpToken(token);
} catch (error) {
throw new Error(`Invalid MCP token: ${error.message}`);
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
const opts = {
method,
headers,
...(body ? { body: JSON.stringify(body) } : {})
};
const res = await fetch(url, opts);
if (!res.ok) {
let errText = await res.text();
// Enhanced error handling for MCP authentication
if (res.status === 401) {
throw new Error('Unauthorized: Your MCP token is invalid or expired. Please generate a new token via the API key endpoint.');
}
if (res.status === 403) {
throw new Error('Forbidden: Your MCP token does not have the required permissions.');
}
throw new Error(`HTTP ${res.status}: ${errText}`);
}
return await res.json();
}
const server = new McpServer({
name: "logggai-mcp",
version: "0.2.8"
});
// ==== MCP TOOLS ====
// ==== MCP TOOL: list_posts (SaaS HTTP) ====
server.registerTool(
"list_posts",
{
title: "List Posts",
description: "List recent posts from logggai (calls SaaS API). If the user says 'show my recent posts', 'what did I publish', 'show my latest updates', or asks for a list of their latest articles, use this tool to fetch and display their most recent posts.",
inputSchema: {
limit: z.string().optional().describe("Number of articles to display (default: 10)")
},
},
async ({ limit }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/posts${limit ? `?limit=${encodeURIComponent(limit)}` : ''}`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (Array.isArray(data.posts) && data.posts.length > 0) {
text = data.posts.map(p => `• ${p.title} (${p.id}) [${(p.tags || []).join(', ')}]`).join('\n');
} else {
text = 'No posts found. Please create a post first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "list_posts", args: { limit }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "list_posts", args: { limit }, error });
return result;
}
}
);
// ==== END MCP TOOL: list_posts ====
// ==== MCP TOOL: create_post (SaaS HTTP) ====
server.registerTool(
"create_post",
{
title: "Create Post",
description: "Create a new article/post in Logggai (calls SaaS API). If the user says 'create a post', 'write an article', 'share an update', or 'draft a post about today’s release', use this tool to create a new post with the provided content and options.",
inputSchema: {
title: z.string().describe("Post title (required)"),
content: z.string().describe("Post content (required)"),
tags: z.array(z.string()).optional().describe("Tags (optional, array of strings)"),
useAI: z.boolean().optional().describe("Enhance with AI (optional)"),
promptId: z.string().optional().describe("AI prompt ID (optional). Run the list_prompts tool to see all available prompts, then copy and paste the prompt ID here. Example: 1234-5678-..."),
organizationId: z.string().optional().describe("Organization ID (optional, for org context). You can get an organization ID by running the get_organizations tool.")
},
},
async ({ title, content, tags, useAI, promptId, organizationId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/posts`;
const body = {
title,
content,
tags,
useAI,
promptId,
organizationId,
};
Object.keys(body).forEach(key => body[key] === undefined && delete body[key]);
const data = await fetchSaas({ method: 'POST', url, body });
let text;
if (data && data.success && data.post) {
text = `✓ Post created: ${data.post.title} (ID: ${data.post.id})`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error creating post.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "create_post", args: { title, content, tags, useAI, promptId, organizationId }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "create_post", args: { title, content, tags, useAI, promptId, organizationId }, error });
return result;
}
}
);
// ==== END MCP TOOL: create_post ====
// ==== MCP TOOL: list_projects (SaaS HTTP) ====
server.registerTool(
"list_projects",
{
title: "List Projects",
description: "List projects from logggai (calls SaaS API). If the user says 'show my projects', 'list my workspaces', 'what projects do I have', or asks for a summary of their projects, use this tool to fetch and display all available projects for the current context.",
inputSchema: {
contextType: z.string().optional().describe("Context type: 'personal' or 'organization' (default: 'personal')"),
contextId: z.string().optional().describe("Context ID (organizationId, optional)")
},
},
async ({ contextType = 'personal', contextId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
let url = `https://logggai.run/api/projects?contextType=${encodeURIComponent(contextType)}`;
if (contextType === 'organization' && contextId) {
url += `&contextId=${encodeURIComponent(contextId)}`;
}
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && Array.isArray(data.projects) && data.projects.length > 0) {
text = data.projects.map(p => `• ${p.name} (${p.id})`).join('\n');
} else {
text = 'No projects found. Please create a project first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "list_projects", args: { contextType, contextId }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "list_projects", args: { contextType, contextId }, error });
return result;
}
}
);
// ==== END MCP TOOL: list_projects ====
// ==== MCP TOOL: get_status (SaaS HTTP) ====
server.registerTool(
"get_status",
{
title: "Get Status",
description: "Get health/status of the Logggai SaaS API (calls SaaS API). If the user asks 'is the service up', 'status of logggai', or 'is the API working', use this tool to check the current health of the platform.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/health`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && data.status === 'ok') {
text = `Status: ok\nService: ${data.service}\nVersion: ${data.version}\nTimestamp: ${data.timestamp}`;
} else {
text = `Status: ${data.status || 'unknown'}\nError: ${data.error || 'No details'}`;
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_status", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_status", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: get_status ====
// ==== MCP TOOL: list_prompts (SaaS HTTP) ====
server.registerTool(
"list_prompts",
{
title: "List Prompts",
description: "List available AI prompts from logggai (calls SaaS API). If the user says 'show available prompts', 'what AI templates can I use', or 'list writing styles', use this tool to display all prompts.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/prompts`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && Array.isArray(data.prompts) && data.prompts.length > 0) {
text = data.prompts.map(p => `• ${p.name} (${p.id})`).join('\n');
} else {
text = 'No prompts found. Please create a prompt first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "list_prompts", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "list_prompts", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: list_prompts ====
// ==== MCP TOOL: publish_post (SaaS HTTP) ====
server.registerTool(
"publish_post",
{
title: "Publish Post",
description: "Publish an article to social platforms (calls SaaS API). If the user says 'publish this on LinkedIn', 'share to Notion', 'send to Jira', 'publish on Linear', 'post on Hashnode', or generally wants to share their article externally, use this tool with the article ID and the target platforms (LinkedIn, Notion, Jira, Linear, Hashnode).",
inputSchema: {
articleId: z.string().describe("ID of the article to publish (required). You can get an article ID by running the list_posts tool or after creating a post."),
platforms: z.array(z.string()).describe("Platforms to publish to (required, array of strings). Use get_integrations to see available platforms."),
options: z.object({}).optional().describe("Additional publish options (optional, object)")
},
},
async ({ articleId, platforms, options }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
// Fetch the post to check content before publishing
const postData = await fetchSaas({ method: 'GET', url: `https://logggai.run/api/posts/${encodeURIComponent(articleId)}` });
const post = postData?.post || postData;
if (!post || !post.title || !post.content || post.content.trim().length < 2) {
return {
content: [{ type: "text", text: "Cannot publish: post must have a title and non-empty content (at least 2 characters) to publish to Notion or other platforms." }],
isError: true
};
}
const url = `https://logggai.run/api/integrations/publish`;
const body = { articleId, platforms, options };
Object.keys(body).forEach(key => body[key] === undefined && delete body[key]);
const data = await fetchSaas({ method: 'POST', url, body });
let text;
if (data && data.success && Array.isArray(data.results)) {
text = data.results.map(r => r.success ? `${r.platform}` : `${r.platform}: ${r.error || 'Unknown error'}`).join('\n');
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error publishing post.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "publish_post", args: { articleId, platforms, options }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "publish_post", args: { articleId, platforms, options }, error });
return result;
}
}
);
// ==== END MCP TOOL: publish_post ====
// ==== MCP TOOL: get_cli_doc (SaaS HTTP) ====
server.registerTool(
"get_cli_doc",
{
title: "Get CLI Doc",
description: "Get CLI documentation (calls SaaS public endpoint). If the user says 'show CLI help', 'how do I use the CLI', or 'list CLI commands', use this tool to fetch and display the documentation.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/cli-doc.txt`;
// fetchSaas attend du JSON, mais ici on veut du texte brut
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
const text = await res.text();
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_cli_doc", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_cli_doc", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: get_cli_doc ====
// ==== MCP TOOL: get_integrations (SaaS HTTP) ====
server.registerTool(
"get_integrations",
{
title: "Get Integrations",
description: "List user integrations from logggai (calls SaaS API). If the user says 'what integrations are connected', 'show my integrations', or 'which platforms are linked', use this tool to list all connected services.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/integrations`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && Array.isArray(data.integrations) && data.integrations.length > 0) {
text = data.integrations.map(i => `• ${i.platform} (${i.isConnected ? 'connected' : 'not connected'})`).join('\n');
} else {
text = 'No integrations found. Please connect an integration first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_integrations", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_integrations", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: get_integrations ====
// ==== MCP TOOL: get_organizations (SaaS HTTP) ====
server.registerTool(
"get_organizations",
{
title: "Get Organizations",
description: "List user organizations from logggai (calls SaaS API). If the user says 'show my organizations', 'list my teams', or 'what orgs am I part of', use this tool to display all organizations for the user.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/organizations`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && Array.isArray(data.organizations) && data.organizations.length > 0) {
text = data.organizations.map(o => `• ${o.name} (${o.id})`).join('\n');
} else {
text = 'No organizations found. Please create an organization first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_organizations", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_organizations", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: get_organizations ====
// ==== MCP TOOL: switch_context (SaaS HTTP) ====
server.registerTool(
"switch_context",
{
title: "Switch Context",
description: "Switch user context (personal/org) in logggai (calls SaaS API). If the user says 'switch to my organization', 'change workspace', or 'work in personal mode', use this tool to change the current context.",
inputSchema: {
organizationId: z.string().optional().describe("Organization ID to switch to (optional, null or omitted = personal context). You can get an organization ID by running the get_organizations tool.")
},
},
async ({ organizationId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/user/context`;
const body = organizationId ? { organizationId } : {};
const data = await fetchSaas({ method: 'POST', url, body });
let text;
if (data && data.message) {
text = `${data.message}`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error switching context.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "switch_context", args: { organizationId }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "switch_context", args: { organizationId }, error });
return result;
}
}
);
// ==== END MCP TOOL: switch_context ====
// ==== MCP TOOL: get_default_prompt (SaaS HTTP) ====
server.registerTool(
"get_default_prompt",
{
title: "Get Default Prompt",
description: "Get user default AI prompt from logggai (calls SaaS API). If the user asks 'what is my default prompt', 'show my AI settings', 'which prompt do I use by default', or 'what's my current AI template', use this tool to retrieve the default prompt configuration.",
inputSchema: {},
},
async () => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/user/default-prompt`;
const data = await fetchSaas({ method: 'GET', url });
let text;
if (data && typeof data.promptId !== 'undefined') {
text = data.promptId ? `Default prompt ID: ${data.promptId}` : 'No default prompt set.';
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error fetching default prompt.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_default_prompt", args: {}, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_default_prompt", args: {}, error });
return result;
}
}
);
// ==== END MCP TOOL: get_default_prompt ====
// ==== MCP TOOL: sync_commits (SaaS HTTP) ====
server.registerTool(
"sync_commits",
{
title: "Sync Commits",
description: "Sync git commits for the current project (calls SaaS API). If the user says 'sync my commits', 'import my git history', or 'generate posts from my code activity', use this tool with the project ID and commit list.",
inputSchema: {
projectId: z.string().describe("Project ID (required). You can get a project ID by running the list_projects tool."),
commits: z.array(z.object({})).describe("List of commits to sync (required, array of commit objects with hash, message, author, date, etc.)"),
contextType: z.string().optional().describe("Context type: 'personal' or 'organization' (optional)"),
contextId: z.string().optional().describe("Context ID (organizationId, optional)")
},
},
async ({ projectId, commits, contextType, contextId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/sync`;
const body = { projectId, commits, contextType, contextId };
Object.keys(body).forEach(key => body[key] === undefined && delete body[key]);
const data = await fetchSaas({ method: 'POST', url, body });
let text;
if (data && Array.isArray(data.posts)) {
text = `${data.posts.length} post(s) created from commits.`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error syncing commits.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "sync_commits", args: { projectId, commits, contextType, contextId }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "sync_commits", args: { projectId, commits, contextType, contextId }, error });
return result;
}
}
);
// ==== END MCP TOOL: sync_commits ====
// ==== MCP TOOL: update_post (SaaS HTTP) ====
server.registerTool(
"update_post",
{
title: "Update Post",
description: "Update an existing article/post in Logggai (calls SaaS API). If the user says 'edit my post', 'update article', or wants to change the content/tags of an existing post, use this tool with the post ID and new data. Use action: 'archive' or 'unarchive' to archive/unarchive.",
inputSchema: {
articleId: z.string().describe("ID of the article to update (required). You can get an article ID by running the list_posts tool."),
title: z.string().optional().describe("New post title (optional)"),
content: z.string().optional().describe("New post content (optional)"),
tags: z.array(z.string()).optional().describe("Tags (optional, array of strings)"),
useAI: z.boolean().optional().describe("Enhance with AI (optional)"),
promptId: z.string().optional().describe("AI prompt ID (optional). Run the list_prompts tool to see all available prompts, then copy and paste the prompt ID here. Example: 1234-5678-..."),
organizationId: z.string().optional().describe("Organization ID (optional, for org context). You can get an organization ID by running the get_organizations tool."),
action: z.string().optional().describe("Action to perform: 'archive' or 'unarchive' (optional)")
},
},
async ({ articleId, title, content, tags, useAI, promptId, organizationId, action }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
let url = `https://logggai.run/api/posts/${encodeURIComponent(articleId)}`;
let data, text;
if (action && (action === 'archive' || action === 'unarchive')) {
// PATCH pour archiver/désarchiver
const body = { action };
data = await fetchSaas({ method: 'PATCH', url, body });
if (data && data.id) {
text = `✓ Post ${action}d: ${data.title} (ID: ${data.id})`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error archiving/unarchiving post.';
}
} else {
// PUT pour édition complète
const body = {};
if (title !== undefined) body.title = title;
if (content !== undefined) body.content = content;
if (tags !== undefined) body.tags = tags;
if (useAI !== undefined) body.useAI = useAI;
if (promptId !== undefined) body.promptId = promptId;
if (organizationId !== undefined) body.organizationId = organizationId;
data = await fetchSaas({ method: 'PUT', url, body });
if (data && (data.id || data.post)) {
const post = data.post || data;
text = `✓ Post updated: ${post.title} (ID: ${post.id})`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error updating post.';
}
}
logAudit({ tool: "update_post", args: { articleId, title, content, tags, useAI, promptId, organizationId, action }, result: { content: [{ type: "text", text }] } });
return { content: [{ type: "text", text }], post: data?.post || data };
} catch (err) {
logAudit({ tool: "update_post", args: { articleId, title, content, tags, useAI, promptId, organizationId, action }, error: err, isError: true });
throw err;
}
}
);
// ==== END MCP TOOL: update_post ====
// ==== MCP TOOL: start_daemon (active) ====
server.registerTool(
"start_daemon",
{
title: "Start Logggai Daemon",
description: "Start the Logggai auto-sync daemon in the specified project directory. If the user says 'start the daemon', 'launch auto-sync', or 'begin monitoring commits', use this tool to directly start the daemon process.",
inputSchema: {
workingDirectory: z.string().optional().describe("Working directory path where the project is located (optional, defaults to current directory)")
},
},
async ({ workingDirectory }) => {
try {
const cwd = workingDirectory || process.cwd();
// Check if project config exists
const projectConfigPath = path.join(cwd, '.logggai-project.json');
if (!fs.existsSync(projectConfigPath)) {
return {
content: [{ type: "text", text: `❌ No .logggai-project.json found in directory: ${cwd}. Please initialize a project first with 'npx logggai project'.` }],
isError: true
};
}
// Check if daemon is already running
const pidFile = path.join(cwd, '.logggai-daemon.pid');
if (fs.existsSync(pidFile)) {
try {
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8'));
process.kill(pid, 0); // throws if not running
return {
content: [{ type: "text", text: `ℹ️ Daemon is already running in this project (PID: ${pid}).` }]
};
} catch {
// PID file exists but process is not running, continue to start
}
}
// Start the daemon using npx logggai start
const { stdout, stderr } = await execAsync('npx logggai start', { cwd });
let text = "✅ Logggai daemon started successfully.";
if (stdout.trim()) {
text += `\n\nOutput:\n${stdout.trim()}`;
}
if (stderr.trim()) {
text += `\n\nWarnings:\n${stderr.trim()}`;
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "start_daemon", args: { workingDirectory }, result });
return result;
} catch (error) {
const result = {
content: [{ type: "text", text: `❌ Failed to start daemon: ${error.message}` }],
isError: true
};
logAudit({ tool: "start_daemon", args: { workingDirectory }, error });
return result;
}
}
);
// ==== END MCP TOOL: start_daemon ====
// ==== MCP TOOL: stop_daemon (active) ====
server.registerTool(
"stop_daemon",
{
title: "Stop Logggai Daemon",
description: "Stop the Logggai auto-sync daemon in the specified project directory. If the user says 'stop the daemon', 'halt auto-sync', or 'stop monitoring commits', use this tool to directly stop the daemon process.",
inputSchema: {
workingDirectory: z.string().optional().describe("Working directory path where the project is located (optional, defaults to current directory)")
},
},
async ({ workingDirectory }) => {
try {
const cwd = workingDirectory || process.cwd();
// Check if project config exists
const projectConfigPath = path.join(cwd, '.logggai-project.json');
if (!fs.existsSync(projectConfigPath)) {
return {
content: [{ type: "text", text: `❌ No .logggai-project.json found in directory: ${cwd}. Please initialize a project first with 'npx logggai project'.` }],
isError: true
};
}
// Check if daemon is running
const pidFile = path.join(cwd, '.logggai-daemon.pid');
if (!fs.existsSync(pidFile)) {
return {
content: [{ type: "text", text: "ℹ️ No daemon is currently running in this project." }]
};
}
// Stop the daemon using npx logggai stop
const { stdout, stderr } = await execAsync('npx logggai stop', { cwd });
let text = "✅ Logggai daemon stopped successfully.";
if (stdout.trim()) {
text += `\n\nOutput:\n${stdout.trim()}`;
}
if (stderr.trim()) {
text += `\n\nWarnings:\n${stderr.trim()}`;
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "stop_daemon", args: { workingDirectory }, result });
return result;
} catch (error) {
const result = {
content: [{ type: "text", text: `❌ Failed to stop daemon: ${error.message}` }],
isError: true
};
logAudit({ tool: "stop_daemon", args: { workingDirectory }, error });
return result;
}
}
);
// ==== END MCP TOOL: stop_daemon ====
// ==== MCP TOOL: daemon_status (active) ====
server.registerTool(
"daemon_status",
{
title: "Check Logggai Daemon Status",
description: "Check the status of the Logggai auto-sync daemon in the specified project directory and show recent log entries. If the user asks 'what is the daemon status', 'is the daemon running', or 'show daemon logs', use this tool to directly check the daemon status.",
inputSchema: {
workingDirectory: z.string().optional().describe("Working directory path where the project is located (optional, defaults to current directory)")
},
},
async ({ workingDirectory }) => {
try {
const cwd = workingDirectory || process.cwd();
// Check if project config exists
const projectConfigPath = path.join(cwd, '.logggai-project.json');
if (!fs.existsSync(projectConfigPath)) {
return {
content: [{ type: "text", text: `❌ No .logggai-project.json found in directory: ${cwd}. Please initialize a project first with 'npx logggai project'.` }],
isError: true
};
}
// Check if daemon is running
const pidFile = path.join(cwd, '.logggai-daemon.pid');
let isRunning = false;
let pid = null;
if (fs.existsSync(pidFile)) {
try {
pid = parseInt(fs.readFileSync(pidFile, 'utf-8'));
process.kill(pid, 0); // throws if not running
isRunning = true;
} catch {
isRunning = false;
}
}
let text = isRunning
? "✅ Logggai daemon is currently running."
: "❌ Logggai daemon is not running.";
// Add PID information if running
if (isRunning && pid) {
text += `\n📋 Process ID: ${pid}`;
}
// Show recent log entries
const logFile = path.join(cwd, '.logggai-daemon.log');
if (fs.existsSync(logFile)) {
try {
const logContent = fs.readFileSync(logFile, 'utf-8');
const lines = logContent.split('\n').filter(Boolean);
const lastLines = lines.slice(-10);
if (lastLines.length > 0) {
text += `\n\n📄 Recent log entries (last 10 lines):\n${lastLines.join('\n')}`;
} else {
text += `\n\n📄 Log file is empty.`;
}
} catch (error) {
text += `\n\n📄 Could not read log file: ${error.message}`;
}
} else {
text += `\n\n📄 No log file found.`;
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "daemon_status", args: { workingDirectory }, result });
return result;
} catch (error) {
const result = {
content: [{ type: "text", text: `❌ Failed to check daemon status: ${error.message}` }],
isError: true
};
logAudit({ tool: "daemon_status", args: { workingDirectory }, error });
return result;
}
}
);
// ==== END MCP TOOL: daemon_status ====
// ==== MCP TOOL: get_post (SaaS HTTP) ====
server.registerTool(
"get_post",
{
title: "Get Post(s)",
description: "Retrieve the full content of one or more posts by ID or by tags (calls SaaS API). If the user says 'summarize my journey' or asks for a summary of their day, fetch posts from the last 12 hours and summarize them.",
inputSchema: {
postIds: z.union([z.string(), z.array(z.string())]).optional().describe("ID or list of post IDs to retrieve (optional)"),
tags: z.union([z.string(), z.array(z.string())]).optional().describe("Tag or list of tags to filter posts (optional)"),
limit: z.number().optional().describe("Maximum number of posts to return (optional)")
},
},
async ({ postIds, tags, limit }) => {
// get_post = full content, list_posts = summary only
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
let posts = [];
// 1. By ID(s)
if (postIds) {
const ids = Array.isArray(postIds) ? postIds : [postIds];
for (const id of ids) {
const url = `https://logggai.run/api/posts/${encodeURIComponent(id)}`;
try {
const data = await fetchSaas({ method: 'GET', url });
if (data && data.post) posts.push(data.post);
else if (data && data.title) posts.push(data); // fallback legacy
else posts.push({ id, error: 'Not found' });
} catch (err) {
posts.push({ id, error: `Not found (${err.message})` });
}
}
}
// 2. By tags
if (tags) {
const tagArray = Array.isArray(tags) ? tags : [tags];
for (const tag of tagArray) {
let url = `https://logggai.run/api/posts?tags=${encodeURIComponent(tag)}`;
if (limit) {
url += `&limit=${encodeURIComponent(limit)}`;
}
try {
const data = await fetchSaas({ method: 'GET', url });
const postsArray = Array.isArray(data?.posts) ? data.posts : (Array.isArray(data?.results) ? data.results : []);
postsArray.forEach(p => posts.push(p));
} catch (err) {
// skip tag if error
}
}
}
// 3. Fallback: no postIds/tags → get latest posts (like list_posts, but with full content)
if (!postIds && !tags) {
const url = `https://logggai.run/api/posts${limit ? `?limit=${encodeURIComponent(limit)}` : ''}`;
const data = await fetchSaas({ method: 'GET', url });
const postsArr = Array.isArray(data.posts) ? data.posts : [];
posts = postsArr;
}
// 4. Limit results (after fallback or tags)
if (limit && posts.length > limit) {
posts = posts.slice(0, limit);
}
let text;
if (posts.length > 0) {
text = posts.map(p => `• ${(p.title || '[No title]')} (${p.id || '[No ID]'}) [${Array.isArray(p.tags) ? p.tags.join(', ') : ''}]\n${p.content ? p.content.substring(0, 500) : ''}`.trim()).join('\n---\n');
} else {
text = 'No posts found. Please create a post first.';
}
const result = { content: [{ type: "text", text }] };
logAudit({ tool: "get_post", args: { postIds, tags, limit }, result });
return result;
} catch (error) {
const result = { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
logAudit({ tool: "get_post", args: { postIds, tags, limit }, error });
return result;
}
}
);
// ==== END MCP TOOL: get_post ====
// ==== MCP TOOL: archive_post (SaaS HTTP) ====
server.registerTool(
"archive_post",
{
title: "Archive Post",
description: "Archive a post in Logggai (calls SaaS API). Requires the post ID (UUID). To archive by tag or title, first filter posts with list_posts or get_post, then pass the ID to this tool.",
inputSchema: {
articleId: z.string().describe("ID of the article to archive (required). You can get an article ID by running the list_posts tool.")
},
},
async ({ articleId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/posts/${encodeURIComponent(articleId)}`;
const body = { action: 'archive' };
const data = await fetchSaas({ method: 'PATCH', url, body });
let text;
if (data && data.id) {
text = `✓ Post archived: ${data.title} (ID: ${data.id})`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error archiving post.';
}
logAudit({ tool: "archive_post", args: { articleId }, result: { content: [{ type: "text", text }] } });
return { content: [{ type: "text", text }], post: data };
} catch (err) {
logAudit({ tool: "archive_post", args: { articleId }, error: err, isError: true });
throw err;
}
}
);
// ==== END MCP TOOL: archive_post ====
// ==== MCP TOOL: unarchive_post (SaaS HTTP) ====
server.registerTool(
"unarchive_post",
{
title: "Unarchive Post",
description: "Unarchive a post in Logggai (calls SaaS API). Requires the post ID (UUID). To unarchive by tag or title, first filter posts with list_posts or get_post, then pass the ID to this tool.",
inputSchema: {
articleId: z.string().describe("ID of the article to unarchive (required). You can get an article ID by running the list_posts tool.")
},
},
async ({ articleId }) => {
const token = process.env.API_KEY;
if (!token) {
throw new Error('[MCP][FATAL] No API_KEY found in environment. Please export API_KEY with a valid JWT MCP token.');
}
try {
const url = `https://logggai.run/api/posts/${encodeURIComponent(articleId)}`;
const body = { action: 'unarchive' };
const data = await fetchSaas({ method: 'PATCH', url, body });
let text;
if (data && data.id) {
text = `✓ Post unarchived: ${data.title} (ID: ${data.id})`;
} else if (data && data.error) {
text = `Error: ${data.error}`;
} else {
text = 'Unknown error unarchiving post.';
}
logAudit({ tool: "unarchive_post", args: { articleId }, result: { content: [{ type: "text", text }] } });
return { content: [{ type: "text", text }], post: data };
} catch (err) {
logAudit({ tool: "unarchive_post", args: { articleId }, error: err, isError: true });
throw err;
}
}
);
// ==== END MCP TOOL: unarchive_post ====
// ==============================================================
// ==== MCP SERVER START ====
const transport = new StdioServerTransport();
await server.connect(transport);
// ==============================================
// ==== MCP SERVER END ====