#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { isAuthenticated } from "./config.js";
import * as api from "./api.js";
const server = new McpServer({
name: "nexus",
version: "0.1.0",
});
// ============================================================================
// Project Tools
// ============================================================================
server.tool(
"nexus_list_projects",
"List all projects in the organization with optional filtering by status or assignment",
{
status: z
.string()
.optional()
.describe("Filter by project status (e.g., IN_DEVELOPMENT, LIVE)"),
assignedTo: z
.string()
.optional()
.describe("Filter by assigned user ID"),
limit: z
.number()
.optional()
.describe("Maximum number of projects to return"),
},
async ({ status, assignedTo, limit }) => {
const projects = await api.listProjects({ status, assignedTo, limit });
return {
content: [
{
type: "text",
text: JSON.stringify(projects, null, 2),
},
],
};
}
);
server.tool(
"nexus_get_project",
"Get detailed information about a specific project",
{
projectId: z
.string()
.describe("Project ID or slug"),
},
async ({ projectId }) => {
const project = await api.getProject(projectId);
return {
content: [
{
type: "text",
text: JSON.stringify(project, null, 2),
},
],
};
}
);
server.tool(
"nexus_create_project",
"Create a new project in Nexus",
{
name: z.string().describe("Project name"),
description: z.string().optional().describe("Project description"),
url: z.string().optional().describe("Project URL"),
templateId: z.string().optional().describe("Template ID to base project on"),
deadline: z.string().optional().describe("Project deadline (ISO date)"),
screenshotBase64: z
.string()
.optional()
.describe("Base64-encoded screenshot image"),
screenshotUrl: z.string().optional().describe("URL to screenshot image"),
status: z.string().optional().describe("Initial project status"),
},
async (params) => {
const project = await api.createProject(params);
return {
content: [
{
type: "text",
text: `Project created successfully:\n${JSON.stringify(project, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_project",
"Update a project's details including test credentials",
{
projectId: z.string().describe("Project ID or slug"),
name: z.string().optional().describe("Updated project name"),
description: z.string().optional().describe("Updated description"),
url: z.string().optional().describe("Updated project URL"),
testCredentials: z
.record(z.unknown())
.optional()
.describe("Test credentials as JSON object"),
setupData: z
.record(z.unknown())
.optional()
.describe("Setup data as JSON object"),
featureCompletion: z
.number()
.min(0)
.max(100)
.optional()
.describe("Feature completion percentage (0-100)"),
productionReadiness: z
.number()
.min(0)
.max(100)
.optional()
.describe("Production readiness percentage (0-100)"),
previewImage: z.string().optional().describe("Preview image URL"),
},
async ({ projectId, ...data }) => {
const project = await api.updateProject(projectId, data);
return {
content: [
{
type: "text",
text: `Project updated:\n${JSON.stringify(project, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_project_status",
"Update a project's status",
{
projectId: z.string().describe("Project ID or slug"),
status: z
.string()
.describe(
"New status: IDEA, CONCEPT_BUCKET, READY_FOR_DEV, IN_DEVELOPMENT, READY_FOR_BUSINESS_REVIEW, READY_FOR_QA, IN_QA, PAYMENT_INTEGRATION, ON_HOLD, READY_FOR_ONBOARDING, WAITING_FOR_API_KEYS, LIVE_PREPARATION, LIVE, ARCHIVED"
),
},
async ({ projectId, status }) => {
const project = await api.updateProjectStatus(projectId, status);
return {
content: [
{
type: "text",
text: `Project status updated to ${status}:\n${JSON.stringify(project, null, 2)}`,
},
],
};
}
);
// ============================================================================
// Bug/Card Tools
// ============================================================================
server.tool(
"nexus_list_bugs",
"Get all bugs/cards for a project with optional filtering",
{
projectId: z.string().describe("Project ID or slug"),
status: z
.string()
.optional()
.describe(
"Filter by status: BACKLOG, TODO, IN_AI_DEV, IN_DEV, IN_REVIEW, NEEDS_VERIFICATION, DONE, BLOCKED, CANCELLED"
),
severity: z
.string()
.optional()
.describe("Filter by severity: LOW, MEDIUM, HIGH, CRITICAL"),
assignedToAI: z
.boolean()
.optional()
.describe("Filter bugs assigned to AI"),
limit: z.number().optional().describe("Maximum number of bugs to return"),
},
async ({ projectId, status, severity, assignedToAI, limit }) => {
const bugs = await api.listBugs(projectId, {
status,
severity,
assignedToAI,
limit,
});
return {
content: [
{
type: "text",
text: JSON.stringify(bugs, null, 2),
},
],
};
}
);
server.tool(
"nexus_create_bug",
"Create a new bug/issue card in a project",
{
projectId: z.string().describe("Project ID or slug"),
title: z.string().describe("Bug title"),
description: z.string().describe("Bug description"),
severity: z
.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"])
.describe("Bug severity"),
steps: z.string().optional().describe("Steps to reproduce"),
expected: z.string().optional().describe("Expected behavior"),
actual: z.string().optional().describe("Actual behavior"),
environment: z.string().optional().describe("Environment info"),
screenshotUrl: z.string().optional().describe("Screenshot URL"),
priority: z
.enum(["LOW", "MEDIUM", "HIGH", "URGENT"])
.optional()
.describe("Priority level"),
labels: z.array(z.string()).optional().describe("Labels/tags"),
assigneeId: z.string().optional().describe("User ID to assign"),
},
async ({ projectId, ...data }) => {
const bug = await api.createBug(projectId, data);
return {
content: [
{
type: "text",
text: `Bug created:\n${JSON.stringify(bug, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_card",
"Update a card's status, assignee, or other fields",
{
cardId: z.string().describe("Card ID"),
title: z.string().optional().describe("Updated title"),
description: z.string().optional().describe("Updated description"),
status: z
.string()
.optional()
.describe(
"New status: BACKLOG, TODO, IN_AI_DEV, IN_DEV, IN_REVIEW, NEEDS_VERIFICATION, DONE, BLOCKED, CANCELLED"
),
priority: z
.string()
.optional()
.describe("New priority: LOW, MEDIUM, HIGH, URGENT"),
labels: z.array(z.string()).optional().describe("Updated labels"),
assigneeId: z.string().optional().describe("New assignee user ID"),
},
async ({ cardId, ...data }) => {
const card = await api.updateCard(cardId, data);
return {
content: [
{
type: "text",
text: `Card updated:\n${JSON.stringify(card, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_add_comment",
"Add a comment to a card",
{
cardId: z.string().describe("Card ID"),
content: z.string().describe("Comment content (markdown supported)"),
mentions: z
.array(z.string())
.optional()
.describe("User IDs to @mention"),
},
async ({ cardId, content, mentions }) => {
const comment = await api.addComment(cardId, { content, mentions });
return {
content: [
{
type: "text",
text: `Comment added:\n${JSON.stringify(comment, null, 2)}`,
},
],
};
}
);
// ============================================================================
// Template Tools
// ============================================================================
server.tool(
"nexus_list_templates",
"List available project templates",
{
kind: z
.string()
.optional()
.describe("Filter by kind: UI, API, FULLSTACK"),
tags: z.array(z.string()).optional().describe("Filter by tags"),
},
async ({ kind, tags }) => {
const templates = await api.listTemplates({ kind, tags });
return {
content: [
{
type: "text",
text: JSON.stringify(templates, null, 2),
},
],
};
}
);
server.tool(
"nexus_create_template",
"Create a new project template",
{
name: z.string().describe("Template name"),
slug: z.string().describe("URL-friendly slug (must be unique)"),
description: z.string().optional().describe("Template description"),
kind: z.enum(["UI", "API", "FULLSTACK"]).describe("Template kind"),
githubRepoUrl: z.string().optional().describe("GitHub repository URL"),
previewImage: z.string().optional().describe("Preview image URL"),
tags: z.array(z.string()).optional().describe("Tags for categorization"),
promptsJson: z
.record(z.unknown())
.optional()
.describe("AI prompts configuration"),
},
async (params) => {
const template = await api.createTemplate(params);
return {
content: [
{
type: "text",
text: `Template created:\n${JSON.stringify(template, null, 2)}`,
},
],
};
}
);
// ============================================================================
// Milestone Tools
// ============================================================================
server.tool(
"nexus_list_milestones",
"List milestones with optional date range and type filtering",
{
startDate: z.string().optional().describe("Filter from date (ISO format)"),
endDate: z.string().optional().describe("Filter to date (ISO format)"),
type: z
.string()
.optional()
.describe("Filter by type: GENERAL, ONBOARDING, LAUNCH, REVIEW, DEADLINE"),
completed: z.boolean().optional().describe("Filter by completion status"),
},
async ({ startDate, endDate, type, completed }) => {
const milestones = await api.listMilestones({
startDate,
endDate,
type,
completed,
});
return {
content: [
{
type: "text",
text: JSON.stringify(milestones, null, 2),
},
],
};
}
);
server.tool(
"nexus_create_milestone",
"Create a new milestone",
{
title: z.string().describe("Milestone title"),
date: z.string().describe("Milestone date (ISO format)"),
description: z.string().optional().describe("Description"),
type: z
.string()
.optional()
.describe("Type: GENERAL, ONBOARDING, LAUNCH, REVIEW, DEADLINE"),
priority: z.string().optional().describe("Priority: HIGH, MEDIUM, LOW"),
color: z.string().optional().describe("Color for calendar display"),
projectIds: z
.array(z.string())
.optional()
.describe("Project IDs to link"),
tasks: z
.array(z.object({ title: z.string(), order: z.number().optional() }))
.optional()
.describe("Subtasks to create"),
businessEntity: z
.string()
.optional()
.describe("Business entity (for ONBOARDING type)"),
paymentProvider: z
.string()
.optional()
.describe("Payment provider (for ONBOARDING type)"),
},
async (params) => {
const milestone = await api.createMilestone(params);
return {
content: [
{
type: "text",
text: `Milestone created:\n${JSON.stringify(milestone, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_milestone",
"Update a milestone",
{
milestoneId: z.string().describe("Milestone ID"),
title: z.string().optional().describe("Updated title"),
description: z.string().optional().describe("Updated description"),
date: z.string().optional().describe("Updated date (ISO format)"),
completed: z.boolean().optional().describe("Mark as complete/incomplete"),
addProjects: z
.array(z.string())
.optional()
.describe("Project IDs to add"),
removeProjects: z
.array(z.string())
.optional()
.describe("Project IDs to remove"),
},
async ({ milestoneId, ...data }) => {
const milestone = await api.updateMilestone(milestoneId, data);
return {
content: [
{
type: "text",
text: `Milestone updated:\n${JSON.stringify(milestone, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_milestone_task",
"Update a milestone subtask",
{
milestoneId: z.string().describe("Milestone ID"),
taskId: z.string().describe("Task ID"),
title: z.string().optional().describe("Updated title"),
completed: z.boolean().optional().describe("Mark as complete/incomplete"),
},
async ({ milestoneId, taskId, ...data }) => {
const task = await api.updateMilestoneTask(milestoneId, taskId, data);
return {
content: [
{
type: "text",
text: `Task updated:\n${JSON.stringify(task, null, 2)}`,
},
],
};
}
);
// ============================================================================
// Concept Tools
// ============================================================================
server.tool(
"nexus_list_concepts",
"List product concepts in the organization",
{
status: z
.string()
.optional()
.describe(
"Filter by status: GENERATING, DRAFT, SELECTED, RESEARCHING, REVIEWED, APPROVED, PROMOTED, ARCHIVED"
),
templateId: z.string().optional().describe("Filter by template ID"),
},
async ({ status, templateId }) => {
const concepts = await api.listConcepts({ status, templateId });
return {
content: [
{
type: "text",
text: JSON.stringify(concepts, null, 2),
},
],
};
}
);
server.tool(
"nexus_get_concept",
"Get detailed information about a concept including PRD",
{
conceptId: z.string().describe("Concept ID or slug"),
},
async ({ conceptId }) => {
const concept = await api.getConcept(conceptId);
return {
content: [
{
type: "text",
text: JSON.stringify(concept, null, 2),
},
],
};
}
);
server.tool(
"nexus_create_concept",
"Create a new product concept",
{
name: z.string().describe("Concept name"),
description: z.string().describe("Brief description"),
problemStatement: z.string().optional().describe("Problem being solved"),
solution: z.string().optional().describe("Proposed solution"),
businessModel: z.string().optional().describe("Business model description"),
targetAudience: z.string().optional().describe("Target audience"),
templateId: z.string().optional().describe("Template to associate"),
},
async (params) => {
const concept = await api.createConcept(params);
return {
content: [
{
type: "text",
text: `Concept created:\n${JSON.stringify(concept, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_update_concept",
"Update a concept",
{
conceptId: z.string().describe("Concept ID"),
name: z.string().optional().describe("Updated name"),
description: z.string().optional().describe("Updated description"),
status: z.string().optional().describe("New status"),
problemStatement: z.string().optional().describe("Updated problem statement"),
solution: z.string().optional().describe("Updated solution"),
prdJson: z.record(z.unknown()).optional().describe("Updated PRD document"),
},
async ({ conceptId, ...data }) => {
const concept = await api.updateConcept(conceptId, data);
return {
content: [
{
type: "text",
text: `Concept updated:\n${JSON.stringify(concept, null, 2)}`,
},
],
};
}
);
server.tool(
"nexus_promote_concept",
"Promote a concept to a full project",
{
conceptId: z.string().describe("Concept ID to promote"),
projectName: z.string().optional().describe("Override project name"),
},
async ({ conceptId, projectName }) => {
const result = await api.promoteConcept(conceptId, { projectName });
return {
content: [
{
type: "text",
text: `Concept promoted to project:\n${JSON.stringify(result.project, null, 2)}`,
},
],
};
}
);
// ============================================================================
// Server Startup
// ============================================================================
export async function startServer(): Promise<void> {
if (!isAuthenticated()) {
console.error("Error: Not authenticated.");
console.error("Run: nexus-mcp login");
console.error("Or set NEXUS_API_KEY environment variable");
process.exit(1);
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Nexus MCP Server running on stdio");
}
// Run if executed directly
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
startServer().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
}