This is page 3 of 8. Use http://codebase.md/cyanheads/atlas-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .clinerules
├── .dockerignore
├── .env.example
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ └── publish.yml
├── .gitignore
├── .ncurc.json
├── .repomixignore
├── automated-tests
│ └── AGENT_TEST_05282025.md
├── CHANGELOG.md
├── CLAUDE.md
├── docker-compose.yml
├── docs
│ └── tree.md
├── examples
│ ├── backup-example
│ │ ├── knowledges.json
│ │ ├── projects.json
│ │ ├── relationships.json
│ │ └── tasks.json
│ ├── deep-research-example
│ │ ├── covington_community_grant_research.md
│ │ └── full-export.json
│ ├── README.md
│ └── webui-example.png
├── LICENSE
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── repomix.config.json
├── scripts
│ ├── clean.ts
│ ├── fetch-openapi-spec.ts
│ ├── make-executable.ts
│ └── tree.ts
├── smithery.yaml
├── src
│ ├── config
│ │ └── index.ts
│ ├── index.ts
│ ├── mcp
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── knowledge
│ │ │ │ └── knowledgeResources.ts
│ │ │ ├── projects
│ │ │ │ └── projectResources.ts
│ │ │ ├── tasks
│ │ │ │ └── taskResources.ts
│ │ │ └── types.ts
│ │ ├── server.ts
│ │ ├── tools
│ │ │ ├── atlas_database_clean
│ │ │ │ ├── cleanDatabase.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_deep_research
│ │ │ │ ├── deepResearch.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_add
│ │ │ │ ├── addKnowledge.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_delete
│ │ │ │ ├── deleteKnowledge.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listKnowledge.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_create
│ │ │ │ ├── createProject.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_delete
│ │ │ │ ├── deleteProject.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listProjects.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_update
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── updateProject.ts
│ │ │ ├── atlas_task_create
│ │ │ │ ├── createTask.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_delete
│ │ │ │ ├── deleteTask.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listTasks.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_update
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── updateTask.ts
│ │ │ └── atlas_unified_search
│ │ │ ├── index.ts
│ │ │ ├── responseFormat.ts
│ │ │ ├── types.ts
│ │ │ └── unifiedSearch.ts
│ │ └── transports
│ │ ├── authentication
│ │ │ └── authMiddleware.ts
│ │ ├── httpTransport.ts
│ │ └── stdioTransport.ts
│ ├── services
│ │ └── neo4j
│ │ ├── backupRestoreService
│ │ │ ├── backupRestoreTypes.ts
│ │ │ ├── backupUtils.ts
│ │ │ ├── exportLogic.ts
│ │ │ ├── importLogic.ts
│ │ │ ├── index.ts
│ │ │ └── scripts
│ │ │ ├── db-backup.ts
│ │ │ └── db-import.ts
│ │ ├── driver.ts
│ │ ├── events.ts
│ │ ├── helpers.ts
│ │ ├── index.ts
│ │ ├── knowledgeService.ts
│ │ ├── projectService.ts
│ │ ├── searchService
│ │ │ ├── fullTextSearchLogic.ts
│ │ │ ├── index.ts
│ │ │ ├── searchTypes.ts
│ │ │ └── unifiedSearchLogic.ts
│ │ ├── taskService.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── types
│ │ ├── errors.ts
│ │ ├── mcp.ts
│ │ └── tool.ts
│ ├── utils
│ │ ├── index.ts
│ │ ├── internal
│ │ │ ├── errorHandler.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ └── requestContext.ts
│ │ ├── metrics
│ │ │ ├── index.ts
│ │ │ └── tokenCounter.ts
│ │ ├── parsing
│ │ │ ├── dateParser.ts
│ │ │ ├── index.ts
│ │ │ └── jsonParser.ts
│ │ └── security
│ │ ├── idGenerator.ts
│ │ ├── index.ts
│ │ ├── rateLimiter.ts
│ │ └── sanitization.ts
│ └── webui
│ ├── index.html
│ ├── logic
│ │ ├── api-service.js
│ │ ├── app-state.js
│ │ ├── config.js
│ │ ├── dom-elements.js
│ │ ├── main.js
│ │ └── ui-service.js
│ └── styling
│ ├── base.css
│ ├── components.css
│ ├── layout.css
│ └── theme.css
├── tsconfig.json
├── tsconfig.typedoc.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/src/webui/styling/layout.css:
--------------------------------------------------------------------------------
```css
1 | /* ==========================================================================
2 | Main Application Layout (#app, header, main, footer)
3 | ========================================================================== */
4 | #app {
5 | position: relative; /* For theme toggle positioning */
6 | max-width: 1000px;
7 | margin: var(--spacing-xl) auto;
8 | padding: var(--spacing-lg) var(--spacing-xl);
9 | background-color: var(--card-bg-color);
10 | border-radius: var(--border-radius-lg);
11 | box-shadow: 0 8px 24px var(--shadow-color);
12 | transition: background-color 0.2s ease-out;
13 | }
14 |
15 | .app-header {
16 | margin-bottom: var(--spacing-xl);
17 | }
18 |
19 | .app-footer {
20 | margin-top: var(--spacing-xl);
21 | padding-top: var(--spacing-lg);
22 | border-top: 1px solid var(--border-color);
23 | }
24 |
25 | /* ==========================================================================
26 | Controls Section (Project Select, Refresh Button)
27 | ========================================================================== */
28 | .controls-section {
29 | margin-bottom: var(--spacing-xl);
30 | }
31 | .controls-container {
32 | display: flex;
33 | align-items: center;
34 | gap: var(--spacing-md);
35 | flex-wrap: wrap;
36 | }
37 |
38 | .controls-container label {
39 | margin-bottom: 0; /* Align with controls */
40 | flex-shrink: 0; /* Prevent label from shrinking */
41 | }
42 |
43 | /* ==========================================================================
44 | Data Sections (Project Details, Tasks, Knowledge)
45 | ========================================================================== */
46 | .data-section {
47 | margin-top: var(--spacing-xl);
48 | padding: var(--spacing-lg);
49 | border: 1px solid var(--border-color);
50 | border-radius: var(--border-radius-md);
51 | background-color: var(--data-section-bg);
52 | transition:
53 | background-color 0.2s ease-out,
54 | border-color 0.2s ease-out;
55 | }
56 |
57 | .section-header {
58 | display: flex;
59 | justify-content: space-between;
60 | align-items: center;
61 | margin-bottom: var(--spacing-md);
62 | flex-wrap: wrap; /* Allow wrapping for view controls */
63 | gap: var(--spacing-md);
64 | }
65 |
66 | .section-header h3 {
67 | margin-bottom: 0; /* Remove bottom margin as it's handled by section-header */
68 | }
69 |
70 | /* Project Details Grid Specifics */
71 | #details-content.details-grid {
72 | display: grid;
73 | grid-template-columns: auto 1fr; /* Label and value */
74 | gap: var(--spacing-sm) var(--spacing-md);
75 | align-items: start; /* Align items to the start of their grid cell */
76 | }
77 |
78 | #details-content.details-grid > .data-item {
79 | display: contents; /* Allow children (strong, div/pre/ul) to participate in the grid */
80 | }
81 |
82 | #details-content.details-grid > .data-item > strong {
83 | /* Label */
84 | font-weight: 500;
85 | color: var(--secondary-text-color);
86 | padding-top: var(--spacing-xs); /* Align with multi-line values better */
87 | grid-column: 1;
88 | }
89 | #details-content.details-grid > .data-item > div,
90 | #details-content.details-grid > .data-item > pre,
91 | #details-content.details-grid > .data-item > ul {
92 | /* Value */
93 | grid-column: 2;
94 | margin-bottom: 0; /* Remove default margin from these elements when in grid */
95 | }
96 | #details-content.details-grid > .data-item > ul {
97 | list-style-position: outside; /* More standard list appearance */
98 | padding-left: var(--spacing-md); /* Indent list items */
99 | margin-top: 0;
100 | }
101 | #details-content.details-grid > .data-item > ul li {
102 | margin-bottom: var(--spacing-xs);
103 | }
104 |
105 | /* General Data Items (Used for Tasks, Knowledge in non-grid layout) */
106 | .data-item {
107 | padding-bottom: var(--spacing-md);
108 | margin-bottom: var(--spacing-md);
109 | border-bottom: 1px solid var(--data-item-border-color);
110 | transition: border-color 0.2s ease-out;
111 | }
112 |
113 | .data-item:last-child {
114 | border-bottom: none;
115 | margin-bottom: 0;
116 | padding-bottom: 0;
117 | }
118 |
119 | .data-item strong {
120 | /* Used in task/knowledge titles */
121 | color: var(--text-color);
122 | font-weight: 600;
123 | display: block; /* Make title take full width */
124 | margin-bottom: var(--spacing-xs);
125 | }
126 |
127 | .data-item div {
128 | /* General content div within a data item */
129 | margin-bottom: var(--spacing-xs);
130 | }
131 |
132 | /* ==========================================================================
133 | Mermaid Diagram Container
134 | ========================================================================== */
135 | .mermaid-container {
136 | width: 100%;
137 | min-height: 300px; /* Adjust as needed */
138 | overflow: auto; /* For larger diagrams */
139 | margin-top: var(--spacing-md);
140 | border: 1px solid var(--border-color);
141 | border-radius: var(--border-radius-md);
142 | padding: var(--spacing-md);
143 | background-color: var(
144 | --card-bg-color
145 | ); /* Match card background for consistency */
146 | box-sizing: border-box; /* Ensure padding and border are included in width/height */
147 | }
148 | .mermaid-container svg {
149 | display: block; /* Remove extra space below SVG */
150 | margin: auto; /* Center if smaller than container */
151 | max-width: 100%; /* Ensure SVG scales down if too wide */
152 | }
153 |
154 | /* ==========================================================================
155 | Task Board Styles
156 | ========================================================================== */
157 | .task-board-grid {
158 | display: flex;
159 | gap: var(--spacing-md);
160 | overflow-x: auto; /* Allow horizontal scrolling for columns */
161 | padding-bottom: var(--spacing-md); /* Space for scrollbar */
162 | min-height: 300px; /* Ensure columns have some height */
163 | }
164 |
165 | .task-board-column {
166 | flex: 0 0 280px; /* Fixed width for columns, adjust as needed */
167 | max-width: 280px;
168 | background-color: var(
169 | --bg-color
170 | ); /* Slightly different from card for distinction */
171 | border-radius: var(--border-radius-md);
172 | padding: var(--spacing-md);
173 | border: 1px solid var(--border-color);
174 | display: flex;
175 | flex-direction: column;
176 | transition: background-color 0.2s ease-out;
177 | }
178 |
179 | .task-board-column h4 {
180 | font-size: 1.1rem;
181 | font-weight: 600;
182 | margin-bottom: var(--spacing-md);
183 | padding-bottom: var(--spacing-sm);
184 | border-bottom: 1px solid var(--border-color);
185 | text-align: center;
186 | }
187 |
188 | .task-board-column-content {
189 | flex-grow: 1;
190 | overflow-y: auto; /* Allow scrolling within a column if many tasks */
191 | display: flex;
192 | flex-direction: column;
193 | gap: var(--spacing-sm);
194 | }
195 |
196 | /* ==========================================================================
197 | Data Explorer Styles
198 | ========================================================================== */
199 | #data-explorer-container .controls-container {
200 | margin-bottom: var(--spacing-lg); /* Space between controls and content */
201 | }
202 |
203 | .explorer-node-list {
204 | max-height: 400px; /* Limit height and allow scrolling */
205 | overflow-y: auto;
206 | border: 1px solid var(--border-color);
207 | border-radius: var(--border-radius-md);
208 | padding: var(--spacing-sm);
209 | margin-bottom: var(--spacing-lg); /* Space before details section */
210 | }
211 |
212 | #data-explorer-details {
213 | /* Uses .details-grid, so existing styles apply.
214 | Can add specific overrides if needed */
215 | margin-top: var(--spacing-lg);
216 | padding-top: var(--spacing-lg);
217 | border-top: 1px solid var(--border-color);
218 | }
219 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_update/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ProjectResponse } from "../../../types/mcp.js";
2 | import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
3 |
4 | /**
5 | * Defines a generic interface for formatting data into a string.
6 | */
7 | interface ResponseFormatter<T> {
8 | format(data: T): string;
9 | }
10 |
11 | /**
12 | * Extends the ProjectResponse to include Neo4j properties structure
13 | */
14 | interface SingleProjectResponse extends ProjectResponse {
15 | properties?: any;
16 | identity?: number;
17 | labels?: string[];
18 | elementId?: string;
19 | }
20 |
21 | /**
22 | * Interface for bulk project update response
23 | */
24 | interface BulkProjectResponse {
25 | success: boolean;
26 | message: string;
27 | updated: (ProjectResponse & {
28 | properties?: any;
29 | identity?: number;
30 | labels?: string[];
31 | elementId?: string;
32 | })[];
33 | errors: {
34 | index: number;
35 | project: {
36 | // Original input for the failed update
37 | id: string;
38 | updates: any;
39 | };
40 | error: {
41 | code: string;
42 | message: string;
43 | details?: any;
44 | };
45 | }[];
46 | }
47 |
48 | /**
49 | * Formatter for individual project modification responses
50 | */
51 | export class SingleProjectUpdateFormatter
52 | implements ResponseFormatter<SingleProjectResponse>
53 | {
54 | format(data: SingleProjectResponse): string {
55 | // Extract project properties from Neo4j structure or direct data
56 | const projectData = data.properties || data;
57 | const {
58 | name,
59 | id,
60 | status,
61 | taskType,
62 | updatedAt,
63 | description,
64 | urls,
65 | completionRequirements,
66 | outputFormat,
67 | createdAt,
68 | } = projectData;
69 |
70 | // Create a structured summary section
71 | const summary =
72 | `Project Modified Successfully\n\n` +
73 | `Project: ${name || "Unnamed Project"}\n` +
74 | `ID: ${id || "Unknown ID"}\n` +
75 | `Status: ${status || "Unknown Status"}\n` +
76 | `Type: ${taskType || "Unknown Type"}\n` +
77 | `Updated: ${updatedAt ? new Date(updatedAt).toLocaleString() : "Unknown Date"}\n`;
78 |
79 | // Create a comprehensive details section
80 | let details = `Project Details:\n`;
81 | const fieldLabels: Record<keyof SingleProjectResponse, string> = {
82 | id: "ID",
83 | name: "Name",
84 | description: "Description",
85 | status: "Status",
86 | urls: "URLs",
87 | completionRequirements: "Completion Requirements",
88 | outputFormat: "Output Format",
89 | taskType: "Task Type",
90 | createdAt: "Created At",
91 | updatedAt: "Updated At",
92 | properties: "Raw Properties",
93 | identity: "Neo4j Identity",
94 | labels: "Neo4j Labels",
95 | elementId: "Neo4j Element ID",
96 | dependencies: "Dependencies",
97 | };
98 | const relevantKeys: (keyof SingleProjectResponse)[] = [
99 | "id",
100 | "name",
101 | "description",
102 | "status",
103 | "taskType",
104 | "completionRequirements",
105 | "outputFormat",
106 | "urls",
107 | "createdAt",
108 | "updatedAt",
109 | ];
110 |
111 | relevantKeys.forEach((key) => {
112 | if (projectData[key] !== undefined && projectData[key] !== null) {
113 | let value = projectData[key];
114 | if (Array.isArray(value)) {
115 | value =
116 | value.length > 0
117 | ? value
118 | .map((item) =>
119 | typeof item === "object" ? JSON.stringify(item) : item,
120 | )
121 | .join(", ")
122 | : "None";
123 | } else if (
124 | typeof value === "string" &&
125 | (key === "createdAt" || key === "updatedAt")
126 | ) {
127 | try {
128 | value = new Date(value).toLocaleString();
129 | } catch (e) {
130 | /* Keep original */
131 | }
132 | }
133 | details += ` ${fieldLabels[key] || key}: ${value}\n`;
134 | }
135 | });
136 |
137 | return `${summary}\n${details}`;
138 | }
139 | }
140 |
141 | /**
142 | * Formatter for bulk project update responses
143 | */
144 | export class BulkProjectUpdateFormatter
145 | implements ResponseFormatter<BulkProjectResponse>
146 | {
147 | format(data: BulkProjectResponse): string {
148 | const { success, message, updated, errors } = data;
149 |
150 | const summary =
151 | `${success && errors.length === 0 ? "Projects Updated Successfully" : "Project Updates Completed"}\n\n` +
152 | `Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
153 | `Summary: ${message}\n` +
154 | `Updated: ${updated.length} project(s)\n` +
155 | `Errors: ${errors.length} error(s)\n`;
156 |
157 | let updatedSection = "";
158 | if (updated.length > 0) {
159 | updatedSection = `\n--- Modified Projects (${updated.length}) ---\n\n`;
160 | updatedSection += updated
161 | .map((project, index) => {
162 | const projectData = project.properties || project;
163 | return (
164 | `${index + 1}. ${projectData.name || "Unnamed Project"} (ID: ${projectData.id || "N/A"})\n` +
165 | ` Status: ${projectData.status || "N/A"}\n` +
166 | ` Updated: ${projectData.updatedAt ? new Date(projectData.updatedAt).toLocaleString() : "N/A"}`
167 | );
168 | })
169 | .join("\n\n");
170 | }
171 |
172 | let errorsSection = "";
173 | if (errors.length > 0) {
174 | errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
175 | errorsSection += errors
176 | .map((errorItem, index) => {
177 | return (
178 | `${index + 1}. Error updating Project ID: "${errorItem.project.id}"\n` +
179 | ` Error Code: ${errorItem.error.code}\n` +
180 | ` Message: ${errorItem.error.message}` +
181 | (errorItem.error.details
182 | ? `\n Details: ${JSON.stringify(errorItem.error.details)}`
183 | : "")
184 | );
185 | })
186 | .join("\n\n");
187 | }
188 |
189 | return `${summary}${updatedSection}${errorsSection}`.trim();
190 | }
191 | }
192 |
193 | /**
194 | * Create a formatted, human-readable response for the atlas_project_update tool
195 | *
196 | * @param data The raw project modification response (SingleProjectResponse or BulkProjectResponse)
197 | * @param isError Whether this response represents an error condition (primarily for single responses)
198 | * @returns Formatted MCP tool response with appropriate structure
199 | */
200 | export function formatProjectUpdateResponse(data: any, isError = false): any {
201 | const isBulkResponse =
202 | data.hasOwnProperty("success") &&
203 | data.hasOwnProperty("updated") &&
204 | data.hasOwnProperty("errors");
205 |
206 | let formattedText: string;
207 | let finalIsError: boolean;
208 |
209 | if (isBulkResponse) {
210 | const formatter = new BulkProjectUpdateFormatter();
211 | const bulkData = data as BulkProjectResponse;
212 | formattedText = formatter.format(bulkData);
213 | finalIsError = !bulkData.success || bulkData.errors.length > 0;
214 | } else {
215 | const formatter = new SingleProjectUpdateFormatter();
216 | // For single response, 'data' is the updated project object.
217 | // 'isError' must be determined by the caller if an error occurred before this point.
218 | formattedText = formatter.format(data as SingleProjectResponse);
219 | finalIsError = isError;
220 | }
221 | return createToolResponse(formattedText, finalIsError);
222 | }
223 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_update/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import {
3 | McpToolResponse,
4 | PriorityLevel,
5 | ResponseFormat,
6 | TaskStatus,
7 | TaskType,
8 | createPriorityLevelEnum,
9 | createResponseFormatEnum,
10 | createTaskStatusEnum,
11 | createTaskTypeEnum,
12 | } from "../../../types/mcp.js";
13 |
14 | export const TaskUpdateSchema = z.object({
15 | id: z.string().describe("Identifier of the existing task to be modified"),
16 | updates: z
17 | .object({
18 | title: z
19 | .string()
20 | .min(5)
21 | .max(150)
22 | .optional()
23 | .describe("Modified task title (5-150 characters)"),
24 | description: z
25 | .string()
26 | .optional()
27 | .describe("Revised task description and requirements"),
28 | priority: createPriorityLevelEnum()
29 | .optional()
30 | .describe("Updated priority level reflecting current importance"),
31 | status: createTaskStatusEnum()
32 | .optional()
33 | .describe("Updated task status reflecting current progress"),
34 | assignedTo: z.string().nullable().optional().describe(
35 | // Allow null for unassignment
36 | "Updated assignee ID for task responsibility (null to unassign)",
37 | ),
38 | urls: z
39 | .array(
40 | z.object({
41 | title: z.string(),
42 | url: z.string(),
43 | }),
44 | )
45 | .optional()
46 | .describe("Modified reference materials and documentation links"),
47 | tags: z
48 | .array(z.string())
49 | .optional()
50 | .describe("Updated categorical labels for task organization"),
51 | completionRequirements: z
52 | .string()
53 | .optional()
54 | .describe("Revised success criteria for task completion"),
55 | outputFormat: z
56 | .string()
57 | .optional()
58 | .describe("Modified deliverable specification for task output"),
59 | taskType: createTaskTypeEnum()
60 | .optional()
61 | .describe("Revised classification for task categorization"),
62 | })
63 | .describe(
64 | "Partial update object containing only fields that need modification",
65 | ),
66 | });
67 |
68 | const SingleTaskUpdateSchema = z
69 | .object({
70 | mode: z.literal("single"),
71 | id: z.string(),
72 | updates: z.object({
73 | title: z.string().min(5).max(150).optional(),
74 | description: z.string().optional(),
75 | priority: createPriorityLevelEnum().optional(),
76 | status: createTaskStatusEnum().optional(),
77 | assignedTo: z.string().nullable().optional(), // Allow null
78 | urls: z
79 | .array(
80 | z.object({
81 | title: z.string(),
82 | url: z.string(),
83 | }),
84 | )
85 | .optional(),
86 | tags: z.array(z.string()).optional(),
87 | completionRequirements: z.string().optional(),
88 | outputFormat: z.string().optional(),
89 | taskType: createTaskTypeEnum().optional(),
90 | }),
91 | responseFormat: createResponseFormatEnum()
92 | .optional()
93 | .default(ResponseFormat.FORMATTED)
94 | .describe(
95 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
96 | ),
97 | })
98 | .describe("Update an individual task with selective field modifications");
99 |
100 | const BulkTaskUpdateSchema = z
101 | .object({
102 | mode: z.literal("bulk"),
103 | tasks: z
104 | .array(
105 | z.object({
106 | id: z.string().describe("Identifier of the task to update"),
107 | updates: z.object({
108 | title: z.string().min(5).max(150).optional(),
109 | description: z.string().optional(),
110 | priority: createPriorityLevelEnum().optional(),
111 | status: createTaskStatusEnum().optional(),
112 | assignedTo: z.string().nullable().optional(), // Allow null
113 | urls: z
114 | .array(
115 | z.object({
116 | title: z.string(),
117 | url: z.string(),
118 | }),
119 | )
120 | .optional(),
121 | tags: z.array(z.string()).optional(),
122 | completionRequirements: z.string().optional(),
123 | outputFormat: z.string().optional(),
124 | taskType: createTaskTypeEnum().optional(),
125 | }),
126 | }),
127 | )
128 | .min(1)
129 | .max(100)
130 | .describe(
131 | "Collection of task updates to be applied in a single transaction",
132 | ),
133 | responseFormat: createResponseFormatEnum()
134 | .optional()
135 | .default(ResponseFormat.FORMATTED)
136 | .describe(
137 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
138 | ),
139 | })
140 | .describe("Update multiple related tasks in a single efficient transaction");
141 |
142 | // Schema shapes for tool registration
143 | export const AtlasTaskUpdateSchemaShape = {
144 | mode: z
145 | .enum(["single", "bulk"])
146 | .describe(
147 | "Operation mode - 'single' for one task, 'bulk' for multiple tasks",
148 | ),
149 | id: z
150 | .string()
151 | .optional()
152 | .describe("Existing task ID to update (required for mode='single')"),
153 | updates: z
154 | .object({
155 | title: z.string().min(5).max(150).optional(),
156 | description: z.string().optional(),
157 | priority: createPriorityLevelEnum().optional(),
158 | status: createTaskStatusEnum().optional(),
159 | assignedTo: z.string().nullable().optional(), // Allow null
160 | urls: z
161 | .array(
162 | z.object({
163 | title: z.string(),
164 | url: z.string(),
165 | }),
166 | )
167 | .optional(),
168 | tags: z.array(z.string()).optional(),
169 | completionRequirements: z.string().optional(),
170 | outputFormat: z.string().optional(),
171 | taskType: createTaskTypeEnum().optional(),
172 | })
173 | .optional()
174 | .describe(
175 | "Object containing fields to modify (only specified fields will be updated) (required for mode='single')",
176 | ),
177 | tasks: z
178 | .array(
179 | z.object({
180 | id: z.string(),
181 | updates: z.object({
182 | title: z.string().min(5).max(150).optional(),
183 | description: z.string().optional(),
184 | priority: createPriorityLevelEnum().optional(),
185 | status: createTaskStatusEnum().optional(),
186 | assignedTo: z.string().nullable().optional(), // Allow null
187 | urls: z
188 | .array(
189 | z.object({
190 | title: z.string(),
191 | url: z.string(),
192 | }),
193 | )
194 | .optional(),
195 | tags: z.array(z.string()).optional(),
196 | completionRequirements: z.string().optional(),
197 | outputFormat: z.string().optional(),
198 | taskType: createTaskTypeEnum().optional(),
199 | }),
200 | }),
201 | )
202 | .optional()
203 | .describe(
204 | "Array of task updates, each containing an ID and updates object (required for mode='bulk')",
205 | ),
206 | responseFormat: createResponseFormatEnum()
207 | .optional()
208 | .describe(
209 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
210 | ),
211 | } as const;
212 |
213 | // Schema for validation
214 | export const AtlasTaskUpdateSchema = z.discriminatedUnion("mode", [
215 | SingleTaskUpdateSchema,
216 | BulkTaskUpdateSchema,
217 | ]);
218 |
219 | export type AtlasTaskUpdateInput = z.infer<typeof AtlasTaskUpdateSchema>;
220 | export type TaskUpdateInput = z.infer<typeof TaskUpdateSchema>;
221 | export type AtlasTaskUpdateResponse = McpToolResponse;
222 |
```
--------------------------------------------------------------------------------
/src/utils/security/rateLimiter.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a generic `RateLimiter` class for implementing rate limiting logic.
3 | * It supports configurable time windows, request limits, and automatic cleanup of expired entries.
4 | * @module src/utils/security/rateLimiter
5 | */
6 | import { environment } from "../../config/index.js";
7 | import { BaseErrorCode, McpError } from "../../types/errors.js";
8 | import { logger, RequestContext, requestContextService } from "../index.js";
9 |
10 | /**
11 | * Defines configuration options for the {@link RateLimiter}.
12 | */
13 | export interface RateLimitConfig {
14 | /** Time window in milliseconds. */
15 | windowMs: number;
16 | /** Maximum number of requests allowed in the window. */
17 | maxRequests: number;
18 | /** Custom error message template. Can include `{waitTime}` placeholder. */
19 | errorMessage?: string;
20 | /** If true, skip rate limiting in development. */
21 | skipInDevelopment?: boolean;
22 | /** Optional function to generate a custom key for rate limiting. */
23 | keyGenerator?: (identifier: string, context?: RequestContext) => string;
24 | /** How often, in milliseconds, to clean up expired entries. */
25 | cleanupInterval?: number;
26 | }
27 |
28 | /**
29 | * Represents an individual entry for tracking requests against a rate limit key.
30 | */
31 | export interface RateLimitEntry {
32 | /** Current request count. */
33 | count: number;
34 | /** When the window resets (timestamp in milliseconds). */
35 | resetTime: number;
36 | }
37 |
38 | /**
39 | * A generic rate limiter class using an in-memory store.
40 | * Controls frequency of operations based on unique keys.
41 | */
42 | export class RateLimiter {
43 | /**
44 | * Stores current request counts and reset times for each key.
45 | * @private
46 | */
47 | private limits: Map<string, RateLimitEntry>;
48 | /**
49 | * Timer ID for periodic cleanup.
50 | * @private
51 | */
52 | private cleanupTimer: NodeJS.Timeout | null = null;
53 |
54 | /**
55 | * Default configuration values.
56 | * @private
57 | */
58 | private static DEFAULT_CONFIG: RateLimitConfig = {
59 | windowMs: 15 * 60 * 1000, // 15 minutes
60 | maxRequests: 100,
61 | errorMessage:
62 | "Rate limit exceeded. Please try again in {waitTime} seconds.",
63 | skipInDevelopment: false,
64 | cleanupInterval: 5 * 60 * 1000, // 5 minutes
65 | };
66 |
67 | /**
68 | * Creates a new `RateLimiter` instance.
69 | * @param config - Configuration options, merged with defaults.
70 | */
71 | constructor(private config: RateLimitConfig) {
72 | this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
73 | this.limits = new Map();
74 | this.startCleanupTimer();
75 | }
76 |
77 | /**
78 | * Starts the periodic timer to clean up expired rate limit entries.
79 | * @private
80 | */
81 | private startCleanupTimer(): void {
82 | if (this.cleanupTimer) {
83 | clearInterval(this.cleanupTimer);
84 | }
85 |
86 | const interval =
87 | this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
88 |
89 | if (interval && interval > 0) {
90 | this.cleanupTimer = setInterval(() => {
91 | this.cleanupExpiredEntries();
92 | }, interval);
93 |
94 | if (this.cleanupTimer.unref) {
95 | this.cleanupTimer.unref(); // Allow Node.js process to exit if only timer active
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Removes expired rate limit entries from the store.
102 | * @private
103 | */
104 | private cleanupExpiredEntries(): void {
105 | const now = Date.now();
106 | let expiredCount = 0;
107 |
108 | for (const [key, entry] of this.limits.entries()) {
109 | if (now >= entry.resetTime) {
110 | this.limits.delete(key);
111 | expiredCount++;
112 | }
113 | }
114 |
115 | if (expiredCount > 0) {
116 | const logContext = requestContextService.createRequestContext({
117 | operation: "RateLimiter.cleanupExpiredEntries",
118 | cleanedCount: expiredCount,
119 | totalRemainingAfterClean: this.limits.size,
120 | });
121 | logger.debug(
122 | `Cleaned up ${expiredCount} expired rate limit entries`,
123 | logContext,
124 | );
125 | }
126 | }
127 |
128 | /**
129 | * Updates the configuration of the rate limiter instance.
130 | * @param config - New configuration options to merge.
131 | */
132 | public configure(config: Partial<RateLimitConfig>): void {
133 | this.config = { ...this.config, ...config };
134 | if (config.cleanupInterval !== undefined) {
135 | this.startCleanupTimer();
136 | }
137 | }
138 |
139 | /**
140 | * Retrieves a copy of the current rate limiter configuration.
141 | * @returns The current configuration.
142 | */
143 | public getConfig(): RateLimitConfig {
144 | return { ...this.config };
145 | }
146 |
147 | /**
148 | * Resets all rate limits by clearing the internal store.
149 | */
150 | public reset(): void {
151 | this.limits.clear();
152 | const logContext = requestContextService.createRequestContext({
153 | operation: "RateLimiter.reset",
154 | });
155 | logger.debug("Rate limiter reset, all limits cleared", logContext);
156 | }
157 |
158 | /**
159 | * Checks if a request exceeds the configured rate limit.
160 | * Throws an `McpError` if the limit is exceeded.
161 | *
162 | * @param key - A unique identifier for the request source.
163 | * @param context - Optional request context for custom key generation.
164 | * @throws {McpError} If the rate limit is exceeded.
165 | */
166 | public check(key: string, context?: RequestContext): void {
167 | if (this.config.skipInDevelopment && environment === "development") {
168 | return;
169 | }
170 |
171 | const limitKey = this.config.keyGenerator
172 | ? this.config.keyGenerator(key, context)
173 | : key;
174 |
175 | const now = Date.now();
176 | const entry = this.limits.get(limitKey);
177 |
178 | if (!entry || now >= entry.resetTime) {
179 | this.limits.set(limitKey, {
180 | count: 1,
181 | resetTime: now + this.config.windowMs,
182 | });
183 | return;
184 | }
185 |
186 | if (entry.count >= this.config.maxRequests) {
187 | const waitTime = Math.ceil((entry.resetTime - now) / 1000);
188 | const errorMessage = (
189 | this.config.errorMessage || RateLimiter.DEFAULT_CONFIG.errorMessage!
190 | ).replace("{waitTime}", waitTime.toString());
191 |
192 | throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, {
193 | waitTimeSeconds: waitTime,
194 | key: limitKey,
195 | limit: this.config.maxRequests,
196 | windowMs: this.config.windowMs,
197 | });
198 | }
199 |
200 | entry.count++;
201 | }
202 |
203 | /**
204 | * Retrieves the current rate limit status for a specific key.
205 | * @param key - The rate limit key.
206 | * @returns Status object or `null` if no entry exists.
207 | */
208 | public getStatus(key: string): {
209 | current: number;
210 | limit: number;
211 | remaining: number;
212 | resetTime: number;
213 | } | null {
214 | const entry = this.limits.get(key);
215 | if (!entry) {
216 | return null;
217 | }
218 | return {
219 | current: entry.count,
220 | limit: this.config.maxRequests,
221 | remaining: Math.max(0, this.config.maxRequests - entry.count),
222 | resetTime: entry.resetTime,
223 | };
224 | }
225 |
226 | /**
227 | * Stops the cleanup timer and clears all rate limit entries.
228 | * Call when the rate limiter is no longer needed.
229 | */
230 | public dispose(): void {
231 | if (this.cleanupTimer) {
232 | clearInterval(this.cleanupTimer);
233 | this.cleanupTimer = null;
234 | }
235 | this.limits.clear();
236 | }
237 | }
238 |
239 | /**
240 | * Default singleton instance of the `RateLimiter`.
241 | * Initialized with default configuration. Use `rateLimiter.configure({})` to customize.
242 | */
243 | export const rateLimiter = new RateLimiter({
244 | windowMs: 15 * 60 * 1000, // Default: 15 minutes
245 | maxRequests: 100, // Default: 100 requests per window
246 | });
247 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_add/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
2 |
3 | /**
4 | * Defines a generic interface for formatting data into a string.
5 | */
6 | interface ResponseFormatter<T> {
7 | format(data: T): string;
8 | }
9 |
10 | /**
11 | * Interface for a single knowledge item response
12 | */
13 | interface SingleKnowledgeResponse {
14 | id: string;
15 | projectId: string;
16 | text: string;
17 | tags?: string[];
18 | domain: string;
19 | citations?: string[];
20 | createdAt: string;
21 | updatedAt: string;
22 | properties?: any; // Neo4j properties if not fully mapped
23 | identity?: number; // Neo4j internal ID
24 | labels?: string[]; // Neo4j labels
25 | elementId?: string; // Neo4j element ID
26 | }
27 |
28 | /**
29 | * Interface for bulk knowledge addition response
30 | */
31 | interface BulkKnowledgeResponse {
32 | success: boolean;
33 | message: string;
34 | created: (SingleKnowledgeResponse & {
35 | properties?: any;
36 | identity?: number;
37 | labels?: string[];
38 | elementId?: string;
39 | })[];
40 | errors: {
41 | index: number;
42 | knowledge: any; // Original input for the failed item
43 | error: {
44 | code: string;
45 | message: string;
46 | details?: any;
47 | };
48 | }[];
49 | }
50 |
51 | /**
52 | * Formatter for single knowledge item addition responses
53 | */
54 | export class SingleKnowledgeFormatter
55 | implements ResponseFormatter<SingleKnowledgeResponse>
56 | {
57 | format(data: SingleKnowledgeResponse): string {
58 | // Extract knowledge properties from Neo4j structure or direct data
59 | const knowledgeData = data.properties || data;
60 | const {
61 | id,
62 | projectId,
63 | domain,
64 | createdAt,
65 | text,
66 | tags,
67 | citations,
68 | updatedAt,
69 | } = knowledgeData;
70 |
71 | // Create a summary section
72 | const summary =
73 | `Knowledge Item Added Successfully\n\n` +
74 | `ID: ${id || "Unknown ID"}\n` +
75 | `Project ID: ${projectId || "Unknown Project"}\n` +
76 | `Domain: ${domain || "Uncategorized"}\n` +
77 | `Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
78 |
79 | // Create a comprehensive details section
80 | const fieldLabels: Record<keyof SingleKnowledgeResponse, string> = {
81 | id: "ID",
82 | projectId: "Project ID",
83 | text: "Content",
84 | tags: "Tags",
85 | domain: "Domain",
86 | citations: "Citations",
87 | createdAt: "Created At",
88 | updatedAt: "Updated At",
89 | // Neo4j specific fields are generally not for direct user display unless needed
90 | properties: "Raw Properties",
91 | identity: "Neo4j Identity",
92 | labels: "Neo4j Labels",
93 | elementId: "Neo4j Element ID",
94 | };
95 |
96 | let details = `Knowledge Item Details\n\n`;
97 |
98 | // Build details as key-value pairs for relevant fields
99 | (Object.keys(fieldLabels) as Array<keyof SingleKnowledgeResponse>).forEach(
100 | (key) => {
101 | if (
102 | knowledgeData[key] !== undefined &&
103 | ["properties", "identity", "labels", "elementId"].indexOf(
104 | key as string,
105 | ) === -1
106 | ) {
107 | // Exclude raw Neo4j fields from default display
108 | let value = knowledgeData[key];
109 |
110 | if (Array.isArray(value)) {
111 | value = value.length > 0 ? value.join(", ") : "None";
112 | } else if (
113 | typeof value === "string" &&
114 | (key === "createdAt" || key === "updatedAt")
115 | ) {
116 | try {
117 | value = new Date(value).toLocaleString();
118 | } catch (e) {
119 | /* Keep original if parsing fails */
120 | }
121 | }
122 |
123 | if (
124 | key === "text" &&
125 | typeof value === "string" &&
126 | value.length > 100
127 | ) {
128 | value = value.substring(0, 100) + "... (truncated)";
129 | }
130 |
131 | details += `${fieldLabels[key]}: ${value}\n`;
132 | }
133 | },
134 | );
135 |
136 | return `${summary}\n${details}`;
137 | }
138 | }
139 |
140 | /**
141 | * Formatter for bulk knowledge addition responses
142 | */
143 | export class BulkKnowledgeFormatter
144 | implements ResponseFormatter<BulkKnowledgeResponse>
145 | {
146 | format(data: BulkKnowledgeResponse): string {
147 | const { success, message, created, errors } = data;
148 |
149 | const summary =
150 | `${success && errors.length === 0 ? "Knowledge Items Added Successfully" : "Knowledge Addition Completed"}\n\n` +
151 | `Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
152 | `Summary: ${message}\n` +
153 | `Added: ${created.length} item(s)\n` +
154 | `Errors: ${errors.length} error(s)\n`;
155 |
156 | let createdSection = "";
157 | if (created.length > 0) {
158 | createdSection = `\n--- Added Knowledge Items (${created.length}) ---\n\n`;
159 | createdSection += created
160 | .map((item, index) => {
161 | const itemData = item.properties || item;
162 | return (
163 | `${index + 1}. ID: ${itemData.id || "N/A"}\n` +
164 | ` Project ID: ${itemData.projectId || "N/A"}\n` +
165 | ` Domain: ${itemData.domain || "N/A"}\n` +
166 | ` Tags: ${itemData.tags ? itemData.tags.join(", ") : "None"}\n` +
167 | ` Created: ${itemData.createdAt ? new Date(itemData.createdAt).toLocaleString() : "N/A"}`
168 | );
169 | })
170 | .join("\n\n");
171 | }
172 |
173 | let errorsSection = "";
174 | if (errors.length > 0) {
175 | errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
176 | errorsSection += errors
177 | .map((errorDetail, index) => {
178 | const itemInput = errorDetail.knowledge;
179 | return (
180 | `${index + 1}. Error for item (Index: ${errorDetail.index})\n` +
181 | ` Input Project ID: ${itemInput?.projectId || "N/A"}\n` +
182 | ` Input Domain: ${itemInput?.domain || "N/A"}\n` +
183 | ` Error Code: ${errorDetail.error.code}\n` +
184 | ` Message: ${errorDetail.error.message}` +
185 | (errorDetail.error.details
186 | ? `\n Details: ${JSON.stringify(errorDetail.error.details)}`
187 | : "")
188 | );
189 | })
190 | .join("\n\n");
191 | }
192 |
193 | return `${summary}${createdSection}${errorsSection}`.trim();
194 | }
195 | }
196 |
197 | /**
198 | * Create a formatted, human-readable response for the atlas_knowledge_add tool
199 | *
200 | * @param data The raw knowledge addition response data (can be SingleKnowledgeResponse or BulkKnowledgeResponse)
201 | * @param isError Whether this response represents an error condition (primarily for single responses if not inherent in data)
202 | * @returns Formatted MCP tool response with appropriate structure
203 | */
204 | export function formatKnowledgeAddResponse(data: any, isError = false): any {
205 | const isBulkResponse =
206 | data.hasOwnProperty("success") &&
207 | data.hasOwnProperty("created") &&
208 | data.hasOwnProperty("errors");
209 |
210 | let formattedText: string;
211 | let finalIsError: boolean;
212 |
213 | if (isBulkResponse) {
214 | const formatter = new BulkKnowledgeFormatter();
215 | formattedText = formatter.format(data as BulkKnowledgeResponse);
216 | finalIsError = !data.success || data.errors.length > 0;
217 | } else {
218 | const formatter = new SingleKnowledgeFormatter();
219 | formattedText = formatter.format(data as SingleKnowledgeResponse);
220 | finalIsError = isError; // For single responses, rely on the passed isError or enhance if data has success field
221 | }
222 | return createToolResponse(formattedText, finalIsError);
223 | }
224 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_create/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { TaskResponse } from "../../../types/mcp.js";
2 | import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
3 |
4 | /**
5 | * Defines a generic interface for formatting data into a string.
6 | */
7 | interface ResponseFormatter<T> {
8 | format(data: T): string;
9 | }
10 |
11 | /**
12 | * Extends the TaskResponse to include Neo4j properties structure
13 | */
14 | interface SingleTaskResponse extends TaskResponse {
15 | properties?: any;
16 | identity?: number;
17 | labels?: string[];
18 | elementId?: string;
19 | }
20 |
21 | /**
22 | * Interface for bulk task creation response
23 | */
24 | interface BulkTaskResponse {
25 | success: boolean;
26 | message: string;
27 | created: (TaskResponse & {
28 | properties?: any;
29 | identity?: number;
30 | labels?: string[];
31 | elementId?: string;
32 | })[];
33 | errors: {
34 | index: number;
35 | task: any; // Original input for the failed task
36 | error: {
37 | code: string;
38 | message: string;
39 | details?: any;
40 | };
41 | }[];
42 | }
43 |
44 | /**
45 | * Formatter for single task creation responses
46 | */
47 | export class SingleTaskFormatter
48 | implements ResponseFormatter<SingleTaskResponse>
49 | {
50 | format(data: SingleTaskResponse): string {
51 | // Extract task properties from Neo4j structure or direct data
52 | const taskData = data.properties || data;
53 | const {
54 | title,
55 | id,
56 | projectId,
57 | status,
58 | priority,
59 | taskType,
60 | createdAt,
61 | description,
62 | assignedTo,
63 | urls,
64 | tags,
65 | completionRequirements,
66 | dependencies,
67 | outputFormat,
68 | updatedAt,
69 | } = taskData;
70 |
71 | // Create a summary section
72 | const summary =
73 | `Task Created Successfully\n\n` +
74 | `Task: ${title || "Unnamed Task"}\n` +
75 | `ID: ${id || "Unknown ID"}\n` +
76 | `Project ID: ${projectId || "Unknown Project"}\n` +
77 | `Status: ${status || "Unknown Status"}\n` +
78 | `Priority: ${priority || "Unknown Priority"}\n` +
79 | `Type: ${taskType || "Unknown Type"}\n` +
80 | `Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
81 |
82 | // Create a comprehensive details section
83 | let details = `Task Details:\n`;
84 | const fieldLabels: Record<keyof SingleTaskResponse, string> = {
85 | id: "ID",
86 | projectId: "Project ID",
87 | title: "Title",
88 | description: "Description",
89 | priority: "Priority",
90 | status: "Status",
91 | assignedTo: "Assigned To",
92 | urls: "URLs",
93 | tags: "Tags",
94 | completionRequirements: "Completion Requirements",
95 | dependencies: "Dependencies",
96 | outputFormat: "Output Format",
97 | taskType: "Task Type",
98 | createdAt: "Created At",
99 | updatedAt: "Updated At",
100 | properties: "Raw Properties",
101 | identity: "Neo4j Identity",
102 | labels: "Neo4j Labels",
103 | elementId: "Neo4j Element ID",
104 | };
105 | const relevantKeys: (keyof SingleTaskResponse)[] = [
106 | "id",
107 | "projectId",
108 | "title",
109 | "description",
110 | "priority",
111 | "status",
112 | "assignedTo",
113 | "urls",
114 | "tags",
115 | "completionRequirements",
116 | "dependencies",
117 | "outputFormat",
118 | "taskType",
119 | "createdAt",
120 | "updatedAt",
121 | ];
122 |
123 | relevantKeys.forEach((key) => {
124 | if (taskData[key] !== undefined && taskData[key] !== null) {
125 | let value = taskData[key];
126 | if (Array.isArray(value)) {
127 | value =
128 | value.length > 0
129 | ? value
130 | .map((item) =>
131 | typeof item === "object" ? JSON.stringify(item) : item,
132 | )
133 | .join(", ")
134 | : "None";
135 | } else if (
136 | typeof value === "string" &&
137 | (key === "createdAt" || key === "updatedAt")
138 | ) {
139 | try {
140 | value = new Date(value).toLocaleString();
141 | } catch (e) {
142 | /* Keep original */
143 | }
144 | }
145 | details += ` ${fieldLabels[key] || key}: ${value}\n`;
146 | }
147 | });
148 |
149 | return `${summary}\n${details}`;
150 | }
151 | }
152 |
153 | /**
154 | * Formatter for bulk task creation responses
155 | */
156 | export class BulkTaskFormatter implements ResponseFormatter<BulkTaskResponse> {
157 | format(data: BulkTaskResponse): string {
158 | const { success, message, created, errors } = data;
159 |
160 | const summary =
161 | `${success && errors.length === 0 ? "Tasks Created Successfully" : "Task Creation Completed"}\n\n` +
162 | `Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
163 | `Summary: ${message}\n` +
164 | `Created: ${created.length} task(s)\n` +
165 | `Errors: ${errors.length} error(s)\n`;
166 |
167 | let createdSection = "";
168 | if (created.length > 0) {
169 | createdSection = `\n--- Created Tasks (${created.length}) ---\n\n`;
170 | createdSection += created
171 | .map((task, index) => {
172 | const taskData = task.properties || task;
173 | return (
174 | `${index + 1}. ${taskData.title || "Unnamed Task"} (ID: ${taskData.id || "N/A"})\n` +
175 | ` Project ID: ${taskData.projectId || "N/A"}\n` +
176 | ` Priority: ${taskData.priority || "N/A"}\n` +
177 | ` Status: ${taskData.status || "N/A"}\n` +
178 | ` Created: ${taskData.createdAt ? new Date(taskData.createdAt).toLocaleString() : "N/A"}`
179 | );
180 | })
181 | .join("\n\n");
182 | }
183 |
184 | let errorsSection = "";
185 | if (errors.length > 0) {
186 | errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
187 | errorsSection += errors
188 | .map((errorItem, index) => {
189 | const taskTitle =
190 | errorItem.task?.title || `Input task at index ${errorItem.index}`;
191 | return (
192 | `${index + 1}. Error for task: "${taskTitle}" (Project ID: ${errorItem.task?.projectId || "N/A"})\n` +
193 | ` Error Code: ${errorItem.error.code}\n` +
194 | ` Message: ${errorItem.error.message}` +
195 | (errorItem.error.details
196 | ? `\n Details: ${JSON.stringify(errorItem.error.details)}`
197 | : "")
198 | );
199 | })
200 | .join("\n\n");
201 | }
202 |
203 | return `${summary}${createdSection}${errorsSection}`.trim();
204 | }
205 | }
206 |
207 | /**
208 | * Create a formatted, human-readable response for the atlas_task_create tool
209 | *
210 | * @param data The raw task creation response data (SingleTaskResponse or BulkTaskResponse)
211 | * @param isError Whether this response represents an error condition (primarily for single responses)
212 | * @returns Formatted MCP tool response with appropriate structure
213 | */
214 | export function formatTaskCreateResponse(data: any, isError = false): any {
215 | const isBulkResponse =
216 | data.hasOwnProperty("success") &&
217 | data.hasOwnProperty("created") &&
218 | data.hasOwnProperty("errors");
219 |
220 | let formattedText: string;
221 | let finalIsError: boolean;
222 |
223 | if (isBulkResponse) {
224 | const formatter = new BulkTaskFormatter();
225 | const bulkData = data as BulkTaskResponse;
226 | formattedText = formatter.format(bulkData);
227 | finalIsError = !bulkData.success || bulkData.errors.length > 0;
228 | } else {
229 | const formatter = new SingleTaskFormatter();
230 | // For single response, 'data' is the created task object.
231 | // 'isError' must be determined by the caller if an error occurred before this point.
232 | formattedText = formatter.format(data as SingleTaskResponse);
233 | finalIsError = isError;
234 | }
235 | return createToolResponse(formattedText, finalIsError);
236 | }
237 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_create/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ProjectResponse } from "../../../types/mcp.js";
2 | import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
3 |
4 | /**
5 | * Defines a generic interface for formatting data into a string.
6 | */
7 | interface ResponseFormatter<T> {
8 | format(data: T): string;
9 | }
10 |
11 | /**
12 | * Extends the ProjectResponse to include Neo4j properties structure
13 | */
14 | interface SingleProjectResponse extends ProjectResponse {
15 | properties?: any;
16 | identity?: number;
17 | labels?: string[];
18 | elementId?: string;
19 | }
20 |
21 | /**
22 | * Interface for bulk project creation response
23 | */
24 | interface BulkProjectResponse {
25 | success: boolean;
26 | message: string;
27 | created: (ProjectResponse & {
28 | properties?: any;
29 | identity?: number;
30 | labels?: string[];
31 | elementId?: string;
32 | })[];
33 | errors: {
34 | index: number;
35 | project: any; // Original input for the failed project
36 | error: {
37 | code: string;
38 | message: string;
39 | details?: any;
40 | };
41 | }[];
42 | }
43 |
44 | /**
45 | * Formatter for single project creation responses
46 | */
47 | export class SingleProjectFormatter
48 | implements ResponseFormatter<SingleProjectResponse>
49 | {
50 | format(data: SingleProjectResponse): string {
51 | // Extract project properties from Neo4j structure or direct data
52 | const projectData = data.properties || data;
53 | const {
54 | name,
55 | id,
56 | status,
57 | taskType,
58 | createdAt,
59 | description,
60 | urls,
61 | completionRequirements,
62 | outputFormat,
63 | updatedAt,
64 | } = projectData;
65 |
66 | // Create a summary section
67 | const summary =
68 | `Project Created Successfully\n\n` +
69 | `Project: ${name || "Unnamed Project"}\n` +
70 | `ID: ${id || "Unknown ID"}\n` +
71 | `Status: ${status || "Unknown Status"}\n` +
72 | `Type: ${taskType || "Unknown Type"}\n` +
73 | `Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
74 |
75 | // Create a comprehensive details section
76 | const fieldLabels: Record<keyof SingleProjectResponse, string> = {
77 | id: "ID",
78 | name: "Name",
79 | description: "Description",
80 | status: "Status",
81 | urls: "URLs",
82 | completionRequirements: "Completion Requirements",
83 | outputFormat: "Output Format",
84 | taskType: "Task Type",
85 | createdAt: "Created At",
86 | updatedAt: "Updated At",
87 | // Neo4j specific fields
88 | properties: "Raw Properties",
89 | identity: "Neo4j Identity",
90 | labels: "Neo4j Labels",
91 | elementId: "Neo4j Element ID",
92 | // Fields from ProjectResponse that might not be in projectData directly if it's just properties
93 | dependencies: "Dependencies", // Assuming ProjectResponse might have this
94 | };
95 |
96 | let details = `Project Details:\n`;
97 |
98 | // Build details as key-value pairs for relevant fields
99 | const relevantKeys: (keyof SingleProjectResponse)[] = [
100 | "id",
101 | "name",
102 | "description",
103 | "status",
104 | "taskType",
105 | "completionRequirements",
106 | "outputFormat",
107 | "urls",
108 | "createdAt",
109 | "updatedAt",
110 | ];
111 |
112 | relevantKeys.forEach((key) => {
113 | if (projectData[key] !== undefined && projectData[key] !== null) {
114 | let value = projectData[key];
115 |
116 | if (Array.isArray(value)) {
117 | value =
118 | value.length > 0
119 | ? value
120 | .map((item) =>
121 | typeof item === "object" ? JSON.stringify(item) : item,
122 | )
123 | .join(", ")
124 | : "None";
125 | } else if (
126 | typeof value === "string" &&
127 | (key === "createdAt" || key === "updatedAt")
128 | ) {
129 | try {
130 | value = new Date(value).toLocaleString();
131 | } catch (e) {
132 | /* Keep original if parsing fails */
133 | }
134 | }
135 |
136 | details += ` ${fieldLabels[key] || key}: ${value}\n`;
137 | }
138 | });
139 |
140 | return `${summary}\n${details}`;
141 | }
142 | }
143 |
144 | /**
145 | * Formatter for bulk project creation responses
146 | */
147 | export class BulkProjectFormatter
148 | implements ResponseFormatter<BulkProjectResponse>
149 | {
150 | format(data: BulkProjectResponse): string {
151 | const { success, message, created, errors } = data;
152 |
153 | const summary =
154 | `${success && errors.length === 0 ? "Projects Created Successfully" : "Project Creation Completed"}\n\n` +
155 | `Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
156 | `Summary: ${message}\n` +
157 | `Created: ${created.length} project(s)\n` +
158 | `Errors: ${errors.length} error(s)\n`;
159 |
160 | let createdSection = "";
161 | if (created.length > 0) {
162 | createdSection = `\n--- Created Projects (${created.length}) ---\n\n`;
163 | createdSection += created
164 | .map((project, index) => {
165 | const projectData = project.properties || project;
166 | return (
167 | `${index + 1}. ${projectData.name || "Unnamed Project"} (ID: ${projectData.id || "N/A"})\n` +
168 | ` Type: ${projectData.taskType || "N/A"}\n` +
169 | ` Status: ${projectData.status || "N/A"}\n` +
170 | ` Created: ${projectData.createdAt ? new Date(projectData.createdAt).toLocaleString() : "N/A"}`
171 | );
172 | })
173 | .join("\n\n");
174 | }
175 |
176 | let errorsSection = "";
177 | if (errors.length > 0) {
178 | errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
179 | errorsSection += errors
180 | .map((errorItem, index) => {
181 | const projectName =
182 | errorItem.project?.name ||
183 | `Input project at index ${errorItem.index}`;
184 | return (
185 | `${index + 1}. Error for project: "${projectName}"\n` +
186 | ` Error Code: ${errorItem.error.code}\n` +
187 | ` Message: ${errorItem.error.message}` +
188 | (errorItem.error.details
189 | ? `\n Details: ${JSON.stringify(errorItem.error.details)}`
190 | : "")
191 | );
192 | })
193 | .join("\n\n");
194 | }
195 |
196 | return `${summary}${createdSection}${errorsSection}`.trim();
197 | }
198 | }
199 |
200 | /**
201 | * Create a formatted, human-readable response for the atlas_project_create tool
202 | *
203 | * @param data The raw project creation response data (SingleProjectResponse or BulkProjectResponse)
204 | * @param isError Whether this response represents an error condition (primarily for single responses)
205 | * @returns Formatted MCP tool response with appropriate structure
206 | */
207 | export function formatProjectCreateResponse(data: any, isError = false): any {
208 | const isBulkResponse =
209 | data.hasOwnProperty("success") &&
210 | data.hasOwnProperty("created") &&
211 | data.hasOwnProperty("errors");
212 |
213 | let formattedText: string;
214 | let finalIsError: boolean;
215 |
216 | if (isBulkResponse) {
217 | const formatter = new BulkProjectFormatter();
218 | const bulkData = data as BulkProjectResponse;
219 | formattedText = formatter.format(bulkData);
220 | finalIsError = !bulkData.success || bulkData.errors.length > 0;
221 | } else {
222 | const formatter = new SingleProjectFormatter();
223 | // For single response, the 'data' is the project object itself.
224 | // 'isError' must be determined by the caller if an error occurred before this point.
225 | // If 'data' represents a successfully created project, isError should be false.
226 | formattedText = formatter.format(data as SingleProjectResponse);
227 | finalIsError = isError;
228 | }
229 | return createToolResponse(formattedText, finalIsError);
230 | }
231 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_create/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import {
3 | McpToolResponse,
4 | PriorityLevel,
5 | ResponseFormat,
6 | TaskStatus,
7 | TaskType,
8 | createPriorityLevelEnum,
9 | createResponseFormatEnum,
10 | createTaskStatusEnum,
11 | createTaskTypeEnum,
12 | } from "../../../types/mcp.js";
13 |
14 | export const TaskSchema = z.object({
15 | id: z.string().optional().describe("Optional client-generated task ID"),
16 | projectId: z
17 | .string()
18 | .describe("ID of the parent project this task belongs to"),
19 | title: z
20 | .string()
21 | .min(5)
22 | .max(150)
23 | .describe(
24 | "Concise task title clearly describing the objective (5-150 characters)",
25 | ),
26 | description: z
27 | .string()
28 | .describe("Detailed explanation of the task requirements and context"),
29 | priority: createPriorityLevelEnum()
30 | .default(PriorityLevel.MEDIUM)
31 | .describe("Importance level"),
32 | status: createTaskStatusEnum()
33 | .default(TaskStatus.TODO)
34 | .describe("Current task state"),
35 | assignedTo: z
36 | .string()
37 | .optional()
38 | .describe("ID of entity responsible for task completion"),
39 | urls: z
40 | .array(
41 | z.object({
42 | title: z.string(),
43 | url: z.string(),
44 | }),
45 | )
46 | .optional()
47 | .describe("Relevant URLs with descriptive titles for reference materials"),
48 | tags: z
49 | .array(z.string())
50 | .optional()
51 | .describe("Categorical labels for organization and filtering"),
52 | completionRequirements: z
53 | .string()
54 | .describe("Specific, measurable criteria that indicate task completion"),
55 | dependencies: z
56 | .array(z.string())
57 | .optional()
58 | .describe(
59 | "Array of existing task IDs that must be completed before this task can begin",
60 | ),
61 | outputFormat: z
62 | .string()
63 | .describe("Required format specification for task deliverables"),
64 | taskType: createTaskTypeEnum()
65 | .or(z.string())
66 | .describe("Classification of task purpose"),
67 | });
68 |
69 | const SingleTaskSchema = z
70 | .object({
71 | mode: z.literal("single"),
72 | id: z.string().optional(),
73 | projectId: z.string(),
74 | title: z.string().min(5).max(150),
75 | description: z.string(),
76 | priority: createPriorityLevelEnum()
77 | .optional()
78 | .default(PriorityLevel.MEDIUM),
79 | status: createTaskStatusEnum().optional().default(TaskStatus.TODO),
80 | assignedTo: z.string().optional(),
81 | urls: z
82 | .array(
83 | z.object({
84 | title: z.string(),
85 | url: z.string(),
86 | }),
87 | )
88 | .optional(),
89 | tags: z.array(z.string()).optional(),
90 | completionRequirements: z.string(),
91 | dependencies: z.array(z.string()).optional(),
92 | outputFormat: z.string(),
93 | taskType: createTaskTypeEnum().or(z.string()),
94 | responseFormat: createResponseFormatEnum()
95 | .optional()
96 | .default(ResponseFormat.FORMATTED)
97 | .describe(
98 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
99 | ),
100 | })
101 | .describe("Creates a single task with comprehensive details and metadata");
102 |
103 | const BulkTaskSchema = z
104 | .object({
105 | mode: z.literal("bulk"),
106 | tasks: z
107 | .array(TaskSchema)
108 | .min(1)
109 | .max(100)
110 | .describe(
111 | "Collection of task definitions to create in a single operation. Each object must include all fields required for single task creation (projectId, title, description, completionRequirements, outputFormat, taskType).",
112 | ),
113 | responseFormat: createResponseFormatEnum()
114 | .optional()
115 | .default(ResponseFormat.FORMATTED)
116 | .describe(
117 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
118 | ),
119 | })
120 | .describe("Create multiple related tasks in a single efficient transaction");
121 |
122 | // Schema shapes for tool registration
123 | export const AtlasTaskCreateSchemaShape = {
124 | mode: z
125 | .enum(["single", "bulk"])
126 | .describe(
127 | "Operation mode - 'single' for creating one detailed task with full specifications, 'bulk' for efficiently creating multiple related tasks in a single transaction",
128 | ),
129 | id: z
130 | .string()
131 | .optional()
132 | .describe(
133 | "Optional client-generated task ID for consistent cross-referencing",
134 | ),
135 | projectId: z
136 | .string()
137 | .optional()
138 | .describe(
139 | "ID of the parent project this task belongs to, establishing the project-task relationship hierarchy (required for mode='single')",
140 | ),
141 | title: z
142 | .string()
143 | .min(5)
144 | .max(150)
145 | .optional()
146 | .describe(
147 | "Concise task title clearly describing the objective (5-150 characters) for display and identification (required for mode='single')",
148 | ),
149 | description: z
150 | .string()
151 | .optional()
152 | .describe(
153 | "Detailed explanation of the task requirements, context, approach, and implementation details (required for mode='single')",
154 | ),
155 | priority: createPriorityLevelEnum()
156 | .optional()
157 | .describe(
158 | "Importance level for task prioritization and resource allocation (Default: medium)",
159 | ),
160 | status: createTaskStatusEnum()
161 | .optional()
162 | .describe(
163 | "Current task workflow state for tracking task lifecycle and progress (Default: todo)",
164 | ),
165 | assignedTo: z
166 | .string()
167 | .optional()
168 | .describe(
169 | "ID of entity responsible for task completion and accountability tracking",
170 | ),
171 | urls: z
172 | .array(
173 | z.object({
174 | title: z.string(),
175 | url: z.string(),
176 | }),
177 | )
178 | .optional()
179 | .describe(
180 | "Array of relevant URLs with descriptive titles for reference materials",
181 | ),
182 | tags: z
183 | .array(z.string())
184 | .optional()
185 | .describe(
186 | "Array of categorical labels for task organization, filtering, and thematic grouping",
187 | ),
188 | completionRequirements: z
189 | .string()
190 | .optional()
191 | .describe(
192 | "Specific, measurable criteria that define when the task is considered complete and ready for verification (required for mode='single')",
193 | ),
194 | dependencies: z
195 | .array(z.string())
196 | .optional()
197 | .describe(
198 | "Array of existing task IDs that must be completed before this task can begin, creating sequential workflow paths and prerequisites",
199 | ),
200 | outputFormat: z
201 | .string()
202 | .optional()
203 | .describe(
204 | "Required format and structure specification for the task's deliverables, artifacts, and documentation (required for mode='single')",
205 | ),
206 | taskType: createTaskTypeEnum()
207 | .or(z.string())
208 | .optional()
209 | .describe(
210 | "Classification of task purpose for workflow organization, filtering, and reporting (required for mode='single')",
211 | ),
212 | tasks: z
213 | .array(TaskSchema)
214 | .min(1)
215 | .max(100)
216 | .optional()
217 | .describe(
218 | "Array of complete task definition objects to create in a single transaction (supports 1-100 tasks, required for mode='bulk'). Each object must include all fields required for single task creation (projectId, title, description, completionRequirements, outputFormat, taskType).",
219 | ),
220 | responseFormat: createResponseFormatEnum()
221 | .optional()
222 | .describe(
223 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
224 | ),
225 | } as const;
226 |
227 | // Schema for validation
228 | export const AtlasTaskCreateSchema = z.discriminatedUnion("mode", [
229 | SingleTaskSchema,
230 | BulkTaskSchema,
231 | ]);
232 |
233 | export type AtlasTaskCreateInput = z.infer<typeof AtlasTaskCreateSchema>;
234 | export type TaskInput = z.infer<typeof TaskSchema>;
235 | export type AtlasTaskCreateResponse = McpToolResponse;
236 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_list/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
2 | import { Project, ProjectListResponse } from "./types.js";
3 |
4 | /**
5 | * Defines a generic interface for formatting data into a string.
6 | */
7 | interface ResponseFormatter<T> {
8 | format(data: T): string;
9 | }
10 |
11 | /**
12 | * Formatter for structured project query responses
13 | */
14 | export class ProjectListFormatter
15 | implements ResponseFormatter<ProjectListResponse>
16 | {
17 | /**
18 | * Get an emoji indicator for the task status
19 | */
20 | private getStatusEmoji(status: string): string {
21 | switch (status) {
22 | case "backlog":
23 | return "📋";
24 | case "todo":
25 | return "📝";
26 | case "in_progress":
27 | return "🔄";
28 | case "completed":
29 | return "✅";
30 | default:
31 | return "❓";
32 | }
33 | }
34 |
35 | /**
36 | * Get a visual indicator for the priority level
37 | */
38 | private getPriorityIndicator(priority: string): string {
39 | switch (priority) {
40 | case "critical":
41 | return "[!!!]";
42 | case "high":
43 | return "[!!]";
44 | case "medium":
45 | return "[!]";
46 | case "low":
47 | return "[-]";
48 | default:
49 | return "[?]";
50 | }
51 | }
52 | format(data: ProjectListResponse): string {
53 | const { projects, total, page, limit, totalPages } = data;
54 |
55 | // Generate result summary section
56 | const summary =
57 | `Project Portfolio\n\n` +
58 | `Total Entities: ${total}\n` +
59 | `Page: ${page} of ${totalPages}\n` +
60 | `Displaying: ${Math.min(limit, projects.length)} project(s) per page\n`;
61 |
62 | if (projects.length === 0) {
63 | return `${summary}\n\nNo project entities matched the specified criteria`;
64 | }
65 |
66 | // Format each project
67 | const projectsSections = projects
68 | .map((project, index) => {
69 | // Access properties directly from the project object
70 | const { name, id, status, taskType, createdAt } = project;
71 |
72 | let projectSection =
73 | `${index + 1}. ${name || "Unnamed Project"}\n\n` +
74 | `ID: ${id || "Unknown ID"}\n` +
75 | `Status: ${status || "Unknown Status"}\n` +
76 | `Type: ${taskType || "Unknown Type"}\n` +
77 | `Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
78 |
79 | // Add project details in plain text format
80 | projectSection += `\nProject Details\n\n`;
81 |
82 | // Add each property with proper formatting, accessing directly from 'project'
83 | if (project.id) projectSection += `ID: ${project.id}\n`;
84 | if (project.name) projectSection += `Name: ${project.name}\n`;
85 | if (project.description)
86 | projectSection += `Description: ${project.description}\n`;
87 | if (project.status) projectSection += `Status: ${project.status}\n`;
88 |
89 | // Format URLs array
90 | if (project.urls) {
91 | const urlsValue =
92 | Array.isArray(project.urls) && project.urls.length > 0
93 | ? project.urls
94 | .map((u) => `${u.title}: ${u.url}`)
95 | .join("\n ") // Improved formatting for URLs
96 | : "None";
97 | projectSection += `URLs: ${urlsValue}\n`;
98 | }
99 |
100 | if (project.completionRequirements)
101 | projectSection += `Completion Requirements: ${project.completionRequirements}\n`;
102 | if (project.outputFormat)
103 | projectSection += `Output Format: ${project.outputFormat}\n`;
104 | if (project.taskType)
105 | projectSection += `Task Type: ${project.taskType}\n`;
106 |
107 | // Format dates
108 | if (project.createdAt) {
109 | const createdDate =
110 | typeof project.createdAt === "string" &&
111 | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(project.createdAt)
112 | ? new Date(project.createdAt).toLocaleString()
113 | : project.createdAt;
114 | projectSection += `Created At: ${createdDate}\n`;
115 | }
116 |
117 | if (project.updatedAt) {
118 | const updatedDate =
119 | typeof project.updatedAt === "string" &&
120 | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(project.updatedAt)
121 | ? new Date(project.updatedAt).toLocaleString()
122 | : project.updatedAt;
123 | projectSection += `Updated At: ${updatedDate}\n`;
124 | }
125 |
126 | // Add tasks if included
127 | if (project.tasks && project.tasks.length > 0) {
128 | projectSection += `\nTasks (${project.tasks.length}):\n`;
129 |
130 | projectSection += project.tasks
131 | .map((task, taskIndex) => {
132 | const taskTitle = task.title || "Unnamed Task";
133 | const taskId = task.id || "Unknown ID";
134 | const taskStatus = task.status || "Unknown Status";
135 | const taskPriority = task.priority || "Unknown Priority";
136 | const taskCreatedAt = task.createdAt
137 | ? new Date(task.createdAt).toLocaleString()
138 | : "Unknown Date";
139 |
140 | const statusEmoji = this.getStatusEmoji(taskStatus);
141 | const priorityIndicator = this.getPriorityIndicator(taskPriority);
142 |
143 | return (
144 | ` ${taskIndex + 1}. ${statusEmoji} ${priorityIndicator} ${taskTitle}\n` +
145 | ` ID: ${taskId}\n` +
146 | ` Status: ${taskStatus}\n` +
147 | ` Priority: ${taskPriority}\n` +
148 | ` Created: ${taskCreatedAt}`
149 | );
150 | })
151 | .join("\n\n");
152 | projectSection += "\n";
153 | }
154 |
155 | // Add knowledge if included
156 | if (project.knowledge && project.knowledge.length > 0) {
157 | projectSection += `\nKnowledge Items (${project.knowledge.length}):\n`;
158 |
159 | projectSection += project.knowledge
160 | .map((item, itemIndex) => {
161 | return (
162 | ` ${itemIndex + 1}. ${item.domain || "Uncategorized"} (ID: ${item.id || "N/A"})\n` +
163 | ` Tags: ${item.tags && item.tags.length > 0 ? item.tags.join(", ") : "None"}\n` +
164 | ` Created: ${item.createdAt ? new Date(item.createdAt).toLocaleString() : "N/A"}\n` +
165 | ` Content Preview: ${item.text || "No content available"}`
166 | ); // Preview already truncated if needed
167 | })
168 | .join("\n\n");
169 | projectSection += "\n";
170 | }
171 |
172 | return projectSection;
173 | })
174 | .join("\n\n----------\n\n");
175 |
176 | // Append pagination metadata for multi-page results
177 | let paginationInfo = "";
178 | if (totalPages > 1) {
179 | paginationInfo =
180 | `\n\nPagination Controls:\n` + // Added colon for clarity
181 | `Viewing page ${page} of ${totalPages}.\n` +
182 | `${page < totalPages ? "Use 'page' parameter to navigate to additional results." : "You are on the last page."}`;
183 | }
184 |
185 | return `${summary}\n\n${projectsSections}${paginationInfo}`;
186 | }
187 | }
188 |
189 | /**
190 | * Create a human-readable formatted response for the atlas_project_list tool
191 | *
192 | * @param data The structured project query response data
193 | * @param isError Whether this response represents an error condition
194 | * @returns Formatted MCP tool response with appropriate structure
195 | */
196 | export function formatProjectListResponse(data: any, isError = false): any {
197 | const formatter = new ProjectListFormatter();
198 | const formattedText = formatter.format(data as ProjectListResponse); // Assuming data is ProjectListResponse
199 | return createToolResponse(formattedText, isError);
200 | }
201 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_list/listProjects.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | KnowledgeService,
3 | ProjectService,
4 | TaskService,
5 | } from "../../../services/neo4j/index.js";
6 | import { BaseErrorCode, McpError } from "../../../types/errors.js";
7 | import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
8 | import {
9 | Project,
10 | ProjectListRequest,
11 | ProjectListResponse,
12 | Knowledge,
13 | Task,
14 | } from "./types.js"; // Import Knowledge and Task
15 |
16 | /**
17 | * Retrieve and filter project entities based on specified criteria
18 | * Provides two query modes: detailed entity retrieval or paginated collection listing
19 | *
20 | * @param request The project query parameters including filters and pagination controls
21 | * @returns Promise resolving to structured project entities with optional related resources
22 | */
23 | export async function listProjects(
24 | request: ProjectListRequest,
25 | ): Promise<ProjectListResponse> {
26 | const reqContext = requestContextService.createRequestContext({
27 | toolName: "listProjects",
28 | requestMode: request.mode,
29 | });
30 | try {
31 | const {
32 | mode = "all",
33 | id,
34 | page = 1,
35 | limit = 20,
36 | includeKnowledge = false,
37 | includeTasks = false,
38 | taskType,
39 | status,
40 | } = request;
41 |
42 | // Parameter validation
43 | if (mode === "details" && !id) {
44 | throw new McpError(
45 | BaseErrorCode.VALIDATION_ERROR,
46 | 'Project identifier is required when using mode="details"',
47 | );
48 | }
49 |
50 | // Sanitize pagination parameters
51 | const validatedPage = Math.max(1, page);
52 | const validatedLimit = Math.min(Math.max(1, limit), 100);
53 |
54 | let projects: Project[] = [];
55 | let total = 0;
56 | let totalPages = 0;
57 |
58 | if (mode === "details") {
59 | // Retrieve specific project entity by identifier
60 | const projectResult = await ProjectService.getProjectById(id!);
61 |
62 | if (!projectResult) {
63 | throw new McpError(
64 | BaseErrorCode.NOT_FOUND,
65 | `Project with identifier ${id} not found`,
66 | );
67 | }
68 |
69 | // Cast to the tool's Project type
70 | projects = [projectResult as Project];
71 | total = 1;
72 | totalPages = 1;
73 | } else {
74 | // Get paginated list of projects with filters
75 | const projectsResult = await ProjectService.getProjects({
76 | status,
77 | taskType,
78 | page: validatedPage,
79 | limit: validatedLimit,
80 | });
81 |
82 | // Cast each project to the tool's Project type
83 | projects = projectsResult.data.map((p) => p as Project);
84 | total = projectsResult.total;
85 | totalPages = projectsResult.totalPages;
86 | }
87 |
88 | // Process knowledge resource associations if requested
89 | if (includeKnowledge && projects.length > 0) {
90 | for (const project of projects) {
91 | if (mode === "details") {
92 | // For detailed view, retrieve comprehensive knowledge resources
93 | const knowledgeResult = await KnowledgeService.getKnowledge({
94 | projectId: project.id, // Access directly
95 | page: 1,
96 | limit: 100, // Reasonable threshold for associated resources
97 | });
98 |
99 | // Add debug logging
100 | logger.info("Knowledge items retrieved", {
101 | ...reqContext,
102 | projectId: project.id, // Access directly
103 | count: knowledgeResult.data.length,
104 | firstItem: knowledgeResult.data[0]
105 | ? JSON.stringify(knowledgeResult.data[0])
106 | : "none",
107 | });
108 |
109 | // Map directly, assuming KnowledgeService returns Neo4jKnowledge objects
110 | project.knowledge = knowledgeResult.data.map((item) => {
111 | // More explicit mapping with debug info
112 | logger.debug("Processing knowledge item", {
113 | ...reqContext,
114 | id: item.id,
115 | domain: item.domain,
116 | textLength: item.text ? item.text.length : 0,
117 | });
118 |
119 | // Cast to the tool's Knowledge type
120 | return item as Knowledge;
121 | });
122 | } else {
123 | // For list mode, get abbreviated knowledge items
124 | const knowledgeResult = await KnowledgeService.getKnowledge({
125 | projectId: project.id, // Access directly
126 | page: 1,
127 | limit: 5, // Just a few for summary view
128 | });
129 |
130 | // Map directly, assuming KnowledgeService returns Neo4jKnowledge objects
131 | project.knowledge = knowledgeResult.data.map((item) => {
132 | // Cast to the tool's Knowledge type, potentially truncating text
133 | const knowledgeItem = item as Knowledge;
134 | return {
135 | ...knowledgeItem,
136 | // Show a preview of the text - increased to 200 characters
137 | text:
138 | item.text && item.text.length > 200
139 | ? item.text.substring(0, 200) + "... (truncated)"
140 | : item.text,
141 | };
142 | });
143 | }
144 | }
145 | }
146 |
147 | // Process task entity associations if requested
148 | if (includeTasks && projects.length > 0) {
149 | for (const project of projects) {
150 | if (mode === "details") {
151 | // For detailed view, retrieve prioritized task entities
152 | const tasksResult = await TaskService.getTasks({
153 | projectId: project.id, // Access directly
154 | page: 1,
155 | limit: 100, // Reasonable threshold for associated entities
156 | sortBy: "priority",
157 | sortDirection: "desc",
158 | });
159 |
160 | // Add debug logging
161 | logger.info("Tasks retrieved for project", {
162 | ...reqContext,
163 | projectId: project.id, // Access directly
164 | count: tasksResult.data.length,
165 | firstItem: tasksResult.data[0]
166 | ? JSON.stringify(tasksResult.data[0])
167 | : "none",
168 | });
169 |
170 | // Map directly, assuming TaskService returns Neo4jTask objects
171 | project.tasks = tasksResult.data.map((item) => {
172 | // Debug info
173 | logger.debug("Processing task item", {
174 | ...reqContext,
175 | id: item.id,
176 | title: item.title,
177 | status: item.status,
178 | priority: item.priority,
179 | });
180 |
181 | // Cast to the tool's Task type
182 | return item as Task;
183 | });
184 | } else {
185 | // For list mode, get abbreviated task items
186 | const tasksResult = await TaskService.getTasks({
187 | projectId: project.id, // Access directly
188 | page: 1,
189 | limit: 5, // Just a few for summary view
190 | sortBy: "priority",
191 | sortDirection: "desc",
192 | });
193 |
194 | // Map directly, assuming TaskService returns Neo4jTask objects
195 | project.tasks = tasksResult.data.map((item) => {
196 | // Cast to the tool's Task type
197 | return item as Task;
198 | });
199 | }
200 | }
201 | }
202 |
203 | // Construct the response
204 | const response: ProjectListResponse = {
205 | projects,
206 | total,
207 | page: validatedPage,
208 | limit: validatedLimit,
209 | totalPages,
210 | };
211 |
212 | logger.info("Project query executed successfully", {
213 | ...reqContext,
214 | mode,
215 | count: projects.length,
216 | total,
217 | includeKnowledge,
218 | includeTasks,
219 | });
220 |
221 | return response;
222 | } catch (error) {
223 | logger.error("Project query execution failed", error as Error, reqContext);
224 |
225 | if (error instanceof McpError) {
226 | throw error;
227 | }
228 |
229 | throw new McpError(
230 | BaseErrorCode.INTERNAL_ERROR,
231 | `Failed to retrieve project entities: ${error instanceof Error ? error.message : String(error)}`,
232 | );
233 | }
234 | }
235 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_add/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import {
4 | createToolExample,
5 | createToolMetadata,
6 | registerTool,
7 | } from "../../../types/tool.js";
8 | import { atlasAddKnowledge } from "./addKnowledge.js";
9 | import { AtlasKnowledgeAddSchemaShape } from "./types.js";
10 |
11 | export const registerAtlasKnowledgeAddTool = (server: McpServer) => {
12 | registerTool(
13 | server,
14 | "atlas_knowledge_add",
15 | "Adds a new knowledge item or multiple items to the system with domain categorization, tagging, and citation support",
16 | AtlasKnowledgeAddSchemaShape,
17 | atlasAddKnowledge,
18 | createToolMetadata({
19 | examples: [
20 | createToolExample(
21 | {
22 | mode: "single",
23 | projectId: "proj_ms_migration",
24 | text: "GraphQL provides significant performance benefits over REST when clients need to request multiple related resources. By allowing clients to specify exactly what data they need in a single request, GraphQL eliminates over-fetching and under-fetching problems common in REST APIs.",
25 | domain: "technical",
26 | tags: ["graphql", "api", "performance", "rest"],
27 | citations: [
28 | "https://graphql.org/learn/best-practices/",
29 | "https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/",
30 | ],
31 | },
32 | `{
33 | "id": "know_graphql_benefits",
34 | "projectId": "proj_ms_migration",
35 | "text": "GraphQL provides significant performance benefits over REST when clients need to request multiple related resources. By allowing clients to specify exactly what data they need in a single request, GraphQL eliminates over-fetching and under-fetching problems common in REST APIs.",
36 | "tags": ["graphql", "api", "performance", "rest"],
37 | "domain": "technical",
38 | "citations": ["https://graphql.org/learn/best-practices/", "https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/"],
39 | "createdAt": "2025-03-23T10:11:24.123Z",
40 | "updatedAt": "2025-03-23T10:11:24.123Z"
41 | }`,
42 | "Add technical knowledge about GraphQL benefits with citations and tags",
43 | ),
44 | createToolExample(
45 | {
46 | mode: "bulk",
47 | knowledge: [
48 | {
49 | projectId: "proj_ui_redesign",
50 | text: "User interviews revealed that 78% of our customers struggle with the current checkout flow, particularly the address entry form which was described as 'confusing' and 'overly complex'.",
51 | domain: "business",
52 | tags: ["user-research", "checkout", "ux-issues"],
53 | },
54 | {
55 | projectId: "proj_ui_redesign",
56 | text: "Industry research shows that automatically formatting phone numbers and credit card fields as users type reduces error rates by approximately 25%. Implementing real-time validation with clear error messages has been shown to decrease form abandonment rates by up to 40%.",
57 | domain: "technical",
58 | tags: ["form-design", "validation", "ux-patterns"],
59 | citations: [
60 | "https://baymard.com/blog/input-mask-form-fields",
61 | "https://www.smashingmagazine.com/2020/03/form-validation-ux-design/",
62 | ],
63 | },
64 | ],
65 | },
66 | `{
67 | "success": true,
68 | "message": "Successfully added 2 knowledge items",
69 | "created": [
70 | {
71 | "id": "know_checkout_research",
72 | "projectId": "proj_ui_redesign",
73 | "text": "User interviews revealed that 78% of our customers struggle with the current checkout flow, particularly the address entry form which was described as 'confusing' and 'overly complex'.",
74 | "tags": ["user-research", "checkout", "ux-issues"],
75 | "domain": "business",
76 | "citations": [],
77 | "createdAt": "2025-03-23T10:11:24.123Z",
78 | "updatedAt": "2025-03-23T10:11:24.123Z"
79 | },
80 | {
81 | "id": "know_form_validation",
82 | "projectId": "proj_ui_redesign",
83 | "text": "Industry research shows that automatically formatting phone numbers and credit card fields as users type reduces error rates by approximately 25%. Implementing real-time validation with clear error messages has been shown to decrease form abandonment rates by up to 40%.",
84 | "tags": ["form-design", "validation", "ux-patterns"],
85 | "domain": "technical",
86 | "citations": ["https://baymard.com/blog/input-mask-form-fields", "https://www.smashingmagazine.com/2020/03/form-validation-ux-design/"],
87 | "createdAt": "2025-03-23T10:11:24.456Z",
88 | "updatedAt": "2025-03-23T10:11:24.456Z"
89 | }
90 | ],
91 | "errors": []
92 | }`,
93 | "Add multiple knowledge items with mixed domains and research findings",
94 | ),
95 | ],
96 | requiredPermission: "knowledge:create",
97 | returnSchema: z.union([
98 | // Single knowledge response
99 | z.object({
100 | id: z.string().describe("Knowledge ID"),
101 | projectId: z.string().describe("Project ID"),
102 | text: z.string().describe("Knowledge content"),
103 | tags: z.array(z.string()).describe("Categorical labels"),
104 | domain: z.string().describe("Knowledge domain"),
105 | citations: z.array(z.string()).describe("Reference sources"),
106 | createdAt: z.string().describe("Creation timestamp"),
107 | updatedAt: z.string().describe("Last update timestamp"),
108 | }),
109 | // Bulk creation response
110 | z.object({
111 | success: z.boolean().describe("Operation success status"),
112 | message: z.string().describe("Result message"),
113 | created: z
114 | .array(
115 | z.object({
116 | id: z.string().describe("Knowledge ID"),
117 | projectId: z.string().describe("Project ID"),
118 | text: z.string().describe("Knowledge content"),
119 | tags: z.array(z.string()).describe("Categorical labels"),
120 | domain: z.string().describe("Knowledge domain"),
121 | citations: z.array(z.string()).describe("Reference sources"),
122 | createdAt: z.string().describe("Creation timestamp"),
123 | updatedAt: z.string().describe("Last update timestamp"),
124 | }),
125 | )
126 | .describe("Created knowledge items"),
127 | errors: z
128 | .array(
129 | z.object({
130 | index: z.number().describe("Index in the knowledge array"),
131 | knowledge: z.any().describe("Original knowledge data"),
132 | error: z
133 | .object({
134 | code: z.string().describe("Error code"),
135 | message: z.string().describe("Error message"),
136 | details: z
137 | .any()
138 | .optional()
139 | .describe("Additional error details"),
140 | })
141 | .describe("Error information"),
142 | }),
143 | )
144 | .describe("Creation errors"),
145 | }),
146 | ]),
147 | rateLimit: {
148 | windowMs: 60 * 1000, // 1 minute
149 | maxRequests: 20, // 20 requests per minute (higher than project creation as knowledge items are typically smaller)
150 | },
151 | }),
152 | );
153 | };
154 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_list/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import {
4 | ResponseFormat,
5 | createResponseFormatEnum,
6 | createToolResponse,
7 | } from "../../../types/mcp.js";
8 | import {
9 | createToolExample,
10 | createToolMetadata,
11 | registerTool,
12 | } from "../../../types/tool.js";
13 | import { listKnowledge } from "./listKnowledge.js";
14 | import { formatKnowledgeListResponse } from "./responseFormat.js";
15 | import { KnowledgeListRequest } from "./types.js"; // Corrected import name
16 |
17 | /**
18 | * Registers the atlas_knowledge_list tool with the MCP server
19 | *
20 | * @param server The MCP server instance
21 | */
22 | export function registerAtlasKnowledgeListTool(server: McpServer): void {
23 | registerTool(
24 | server,
25 | "atlas_knowledge_list",
26 | "Lists knowledge items according to specified filters with tag-based categorization, domain filtering, and full-text search capabilities",
27 | {
28 | projectId: z
29 | .string()
30 | .describe("ID of the project to list knowledge items for (required)"),
31 | tags: z
32 | .array(z.string())
33 | .optional()
34 | .describe(
35 | "Array of tags to filter by (items matching any tag will be included)",
36 | ),
37 | domain: z
38 | .string()
39 | .optional()
40 | .describe("Filter by knowledge domain/category"),
41 | search: z
42 | .string()
43 | .optional()
44 | .describe("Text search query to filter results by content relevance"),
45 | page: z
46 | .number()
47 | .min(1)
48 | .optional()
49 | .default(1)
50 | .describe("Page number for paginated results (Default: 1)"),
51 | limit: z
52 | .number()
53 | .min(1)
54 | .max(100)
55 | .optional()
56 | .default(20)
57 | .describe("Number of results per page, maximum 100 (Default: 20)"),
58 | responseFormat: createResponseFormatEnum()
59 | .optional()
60 | .default(ResponseFormat.FORMATTED)
61 | .describe(
62 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
63 | ),
64 | },
65 | async (input, context) => {
66 | // Process knowledge list request
67 | const validatedInput = input as unknown as KnowledgeListRequest & {
68 | responseFormat?: ResponseFormat;
69 | }; // Corrected type cast
70 | const result = await listKnowledge(validatedInput);
71 |
72 | // Conditionally format response
73 | if (validatedInput.responseFormat === ResponseFormat.JSON) {
74 | return createToolResponse(JSON.stringify(result, null, 2));
75 | } else {
76 | // Return the result using the formatter for rich display
77 | return formatKnowledgeListResponse(result, false);
78 | }
79 | },
80 | createToolMetadata({
81 | examples: [
82 | createToolExample(
83 | {
84 | projectId: "proj_ms_migration",
85 | limit: 5,
86 | },
87 | `{
88 | "knowledge": [
89 | {
90 | "id": "know_saga_pattern",
91 | "projectId": "proj_ms_migration",
92 | "projectName": "Microservice Architecture Migration",
93 | "text": "Distributed transactions must use Saga pattern with compensating actions to maintain data integrity across services",
94 | "tags": ["architecture", "data-integrity", "patterns"],
95 | "domain": "technical",
96 | "citations": ["https://microservices.io/patterns/data/saga.html"],
97 | "createdAt": "2025-03-23T11:22:14.789Z",
98 | "updatedAt": "2025-03-23T11:22:14.789Z"
99 | },
100 | {
101 | "id": "know_rate_limiting",
102 | "projectId": "proj_ms_migration",
103 | "projectName": "Microservice Architecture Migration",
104 | "text": "Rate limiting should be implemented at the API Gateway level using Redis-based token bucket algorithm",
105 | "tags": ["api-gateway", "performance", "security"],
106 | "domain": "technical",
107 | "citations": ["https://www.nginx.com/blog/rate-limiting-nginx/"],
108 | "createdAt": "2025-03-23T12:34:27.456Z",
109 | "updatedAt": "2025-03-23T12:34:27.456Z"
110 | }
111 | ],
112 | "total": 2,
113 | "page": 1,
114 | "limit": 5,
115 | "totalPages": 1
116 | }`,
117 | "Retrieve all knowledge items for a specific project",
118 | ),
119 | createToolExample(
120 | {
121 | projectId: "proj_ms_migration",
122 | domain: "technical",
123 | tags: ["security"],
124 | },
125 | `{
126 | "knowledge": [
127 | {
128 | "id": "know_rate_limiting",
129 | "projectId": "proj_ms_migration",
130 | "projectName": "Microservice Architecture Migration",
131 | "text": "Rate limiting should be implemented at the API Gateway level using Redis-based token bucket algorithm",
132 | "tags": ["api-gateway", "performance", "security"],
133 | "domain": "technical",
134 | "citations": ["https://www.nginx.com/blog/rate-limiting-nginx/"],
135 | "createdAt": "2025-03-23T12:34:27.456Z",
136 | "updatedAt": "2025-03-23T12:34:27.456Z"
137 | }
138 | ],
139 | "total": 1,
140 | "page": 1,
141 | "limit": 20,
142 | "totalPages": 1
143 | }`,
144 | "Filter knowledge items by domain and tags",
145 | ),
146 | createToolExample(
147 | {
148 | projectId: "proj_ms_migration",
149 | search: "data integrity",
150 | },
151 | `{
152 | "knowledge": [
153 | {
154 | "id": "know_saga_pattern",
155 | "projectId": "proj_ms_migration",
156 | "projectName": "Microservice Architecture Migration",
157 | "text": "Distributed transactions must use Saga pattern with compensating actions to maintain data integrity across services",
158 | "tags": ["architecture", "data-integrity", "patterns"],
159 | "domain": "technical",
160 | "citations": ["https://microservices.io/patterns/data/saga.html"],
161 | "createdAt": "2025-03-23T11:22:14.789Z",
162 | "updatedAt": "2025-03-23T11:22:14.789Z"
163 | }
164 | ],
165 | "total": 1,
166 | "page": 1,
167 | "limit": 20,
168 | "totalPages": 1
169 | }`,
170 | "Search knowledge items for specific text content",
171 | ),
172 | ],
173 | requiredPermission: "knowledge:read",
174 | entityType: "knowledge",
175 | returnSchema: z.object({
176 | knowledge: z.array(
177 | z.object({
178 | id: z.string().describe("Knowledge ID"),
179 | projectId: z.string().describe("Project ID"),
180 | projectName: z.string().optional().describe("Project name"),
181 | text: z.string().describe("Knowledge content"),
182 | tags: z.array(z.string()).optional().describe("Categorical labels"),
183 | domain: z.string().describe("Knowledge domain/category"),
184 | citations: z
185 | .array(z.string())
186 | .optional()
187 | .describe("Reference sources"),
188 | createdAt: z.string().describe("Creation timestamp"),
189 | updatedAt: z.string().describe("Last update timestamp"),
190 | }),
191 | ),
192 | total: z
193 | .number()
194 | .describe("Total number of knowledge items matching criteria"),
195 | page: z.number().describe("Current page number"),
196 | limit: z.number().describe("Number of items per page"),
197 | totalPages: z.number().describe("Total number of pages"),
198 | }),
199 | rateLimit: {
200 | windowMs: 60 * 1000, // 1 minute
201 | maxRequests: 30, // 30 requests per minute
202 | },
203 | }),
204 | );
205 | }
206 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_unified_search/unifiedSearch.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | SearchResultItem,
3 | SearchService,
4 | } from "../../../services/neo4j/index.js";
5 | import { PaginatedResult } from "../../../services/neo4j/types.js";
6 | import { BaseErrorCode, McpError } from "../../../types/errors.js";
7 | import { ResponseFormat } from "../../../types/mcp.js";
8 | import { ToolContext } from "../../../types/tool.js";
9 | import { logger, requestContextService } from "../../../utils/index.js";
10 | import { formatUnifiedSearchResponse } from "./responseFormat.js";
11 | import {
12 | UnifiedSearchRequestInput,
13 | UnifiedSearchRequestSchema,
14 | UnifiedSearchResponse,
15 | } from "./types.js";
16 |
17 | export const atlasUnifiedSearch = async (
18 | input: unknown,
19 | context: ToolContext,
20 | ): Promise<any> => {
21 | const reqContext =
22 | context.requestContext ??
23 | requestContextService.createRequestContext({
24 | toolName: "atlasUnifiedSearch",
25 | });
26 | try {
27 | const validatedInput = UnifiedSearchRequestSchema.parse(
28 | input,
29 | ) as UnifiedSearchRequestInput & { responseFormat?: ResponseFormat };
30 |
31 | logger.info("Performing unified search", {
32 | ...reqContext,
33 | input: validatedInput,
34 | });
35 |
36 | if (!validatedInput.value || validatedInput.value.trim() === "") {
37 | throw new McpError(
38 | BaseErrorCode.VALIDATION_ERROR,
39 | "Search value cannot be empty",
40 | { param: "value" },
41 | );
42 | }
43 |
44 | let searchResults: PaginatedResult<SearchResultItem>;
45 | const propertyForSearch = validatedInput.property?.trim();
46 | const entityTypesForSearch = validatedInput.entityTypes || [
47 | "project",
48 | "task",
49 | "knowledge",
50 | ]; // Default if not provided
51 |
52 | // Determine if we should use full-text for the given property and entity type
53 | let shouldUseFullText = false;
54 | if (propertyForSearch) {
55 | const lowerProp = propertyForSearch.toLowerCase();
56 | // Check for specific entityType + property combinations that have dedicated full-text indexes
57 | if (entityTypesForSearch.includes("knowledge") && lowerProp === "text") {
58 | shouldUseFullText = true;
59 | } else if (
60 | entityTypesForSearch.includes("project") &&
61 | (lowerProp === "name" || lowerProp === "description")
62 | ) {
63 | shouldUseFullText = true;
64 | } else if (
65 | entityTypesForSearch.includes("task") &&
66 | (lowerProp === "title" || lowerProp === "description")
67 | ) {
68 | shouldUseFullText = true;
69 | }
70 | // Add other specific full-text indexed fields here if any
71 | } else {
72 | // No specific property, so general full-text search is appropriate across default indexed fields
73 | shouldUseFullText = true;
74 | }
75 |
76 | if (shouldUseFullText) {
77 | logger.info(
78 | `Using full-text search. Property: '${propertyForSearch || "default fields"}'`,
79 | {
80 | ...reqContext,
81 | property: propertyForSearch,
82 | targetEntityTypes: entityTypesForSearch,
83 | effectiveFuzzy: validatedInput.fuzzy === true,
84 | },
85 | );
86 |
87 | const escapeLucene = (str: string) =>
88 | str.replace(/([+\-!(){}\[\]^"~*?:\\\/"])/g, "\\$1");
89 | let luceneQueryValue = escapeLucene(validatedInput.value);
90 |
91 | // If fuzzy is requested for the tool, apply it to the Lucene query
92 | if (validatedInput.fuzzy === true) {
93 | luceneQueryValue = `${luceneQueryValue}~1`;
94 | }
95 | // Note: If propertyForSearch is set (e.g., "text" for "knowledge"),
96 | // SearchService.fullTextSearch will use the appropriate index (e.g., "knowledge_fulltext").
97 | // Lucene itself can handle field-specific queries like "fieldName:term",
98 | // but our SearchService.fullTextSearch is already structured to call specific indexes.
99 | // So, just passing the term (and fuzzy if needed) is correct here.
100 |
101 | logger.debug("Constructed Lucene query value for full-text search", {
102 | ...reqContext,
103 | luceneQueryValue,
104 | });
105 |
106 | searchResults = await SearchService.fullTextSearch(luceneQueryValue, {
107 | entityTypes: entityTypesForSearch,
108 | taskType: validatedInput.taskType,
109 | page: validatedInput.page,
110 | limit: validatedInput.limit,
111 | });
112 | } else {
113 | // propertyForSearch is specified, and it's not one we've decided to use full-text for
114 | // This path implies a regex-based search on a specific, non-full-text-optimized property.
115 | // We want "contains" (fuzzy: true for SearchService.search) by default for this path,
116 | // unless the user explicitly passed fuzzy: false in the tool input.
117 | let finalFuzzyForRegexPath: boolean;
118 | if ((input as any)?.fuzzy === false) {
119 | // User explicitly requested an exact match for the regex search
120 | finalFuzzyForRegexPath = false;
121 | } else {
122 | // User either passed fuzzy: true, or didn't pass fuzzy (in which case Zod default is true,
123 | // and we also want "contains" as the intelligent default for this path).
124 | finalFuzzyForRegexPath = true;
125 | }
126 |
127 | logger.info(
128 | `Using regex-based search for specific property: '${propertyForSearch}'. Effective fuzzy for SearchService.search (true means contains): ${finalFuzzyForRegexPath}`,
129 | {
130 | ...reqContext,
131 | property: propertyForSearch,
132 | targetEntityTypes: entityTypesForSearch,
133 | userInputFuzzy: (input as any)?.fuzzy, // Log what user actually passed, if anything
134 | zodParsedFuzzy: validatedInput.fuzzy, // Log what Zod parsed (with default)
135 | finalFuzzyForRegexPath,
136 | },
137 | );
138 |
139 | searchResults = await SearchService.search({
140 | property: propertyForSearch, // Already trimmed
141 | value: validatedInput.value,
142 | entityTypes: entityTypesForSearch,
143 | caseInsensitive: validatedInput.caseInsensitive, // Pass through
144 | fuzzy: finalFuzzyForRegexPath, // This now correctly defaults to 'true' for "contains"
145 | taskType: validatedInput.taskType,
146 | assignedToUserId: validatedInput.assignedToUserId, // Pass through
147 | page: validatedInput.page,
148 | limit: validatedInput.limit,
149 | });
150 | }
151 |
152 | if (!searchResults || !Array.isArray(searchResults.data)) {
153 | logger.error(
154 | "Search service returned invalid data structure.",
155 | new Error("Invalid search results structure"),
156 | { ...reqContext, searchResultsReceived: searchResults },
157 | );
158 | throw new McpError(
159 | BaseErrorCode.INTERNAL_ERROR,
160 | "Received invalid data structure from search service.",
161 | );
162 | }
163 |
164 | logger.info("Unified search completed successfully", {
165 | ...reqContext,
166 | resultCount: searchResults.data.length,
167 | totalResults: searchResults.total,
168 | });
169 |
170 | const responseData: UnifiedSearchResponse = {
171 | results: searchResults.data,
172 | total: searchResults.total,
173 | page: searchResults.page,
174 | limit: searchResults.limit,
175 | totalPages: searchResults.totalPages,
176 | };
177 |
178 | if (validatedInput.responseFormat === ResponseFormat.JSON) {
179 | return responseData;
180 | } else {
181 | return formatUnifiedSearchResponse(responseData);
182 | }
183 | } catch (error) {
184 | const errorMessage =
185 | error instanceof Error ? error.message : "Unknown error";
186 | // const errorStack = error instanceof Error ? error.stack : undefined; // Already captured by logger
187 | logger.error("Failed to perform unified search", error as Error, {
188 | ...reqContext,
189 | // errorMessage and errorStack are part of the Error object passed to logger
190 | inputReceived: input,
191 | });
192 |
193 | if (error instanceof McpError) {
194 | throw error;
195 | } else {
196 | throw new McpError(
197 | BaseErrorCode.INTERNAL_ERROR,
198 | `Error performing unified search: ${errorMessage}`,
199 | );
200 | }
201 | }
202 | };
203 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | // Imports MUST be at the top level
4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5 | import http from "http";
6 | import { config, environment } from "./config/index.js"; // This loads .env via dotenv.config()
7 | import { initializeAndStartServer } from "./mcp/server.js";
8 | import { closeNeo4jConnection } from "./services/neo4j/index.js";
9 | import { logger, McpLogLevel, requestContextService } from "./utils/index.js";
10 |
11 | /**
12 | * The main MCP server instance, stored if transport is stdio for shutdown.
13 | * Or the HTTP server instance if transport is http.
14 | * @type {McpServer | http.Server | undefined}
15 | */
16 | let serverInstance: McpServer | http.Server | undefined;
17 |
18 | /**
19 | * Gracefully shuts down the main MCP server and related services.
20 | * Handles process termination signals (SIGTERM, SIGINT) and critical errors.
21 | *
22 | * @param signal - The signal or event name that triggered the shutdown (e.g., "SIGTERM", "uncaughtException").
23 | */
24 | const shutdown = async (signal: string) => {
25 | // Create a proper RequestContext for shutdown operations
26 | const shutdownContext = requestContextService.createRequestContext({
27 | operation: "Shutdown",
28 | signal,
29 | appName: config.mcpServerName,
30 | });
31 |
32 | logger.info(
33 | `Received ${signal}. Starting graceful shutdown for ${config.mcpServerName}...`,
34 | shutdownContext,
35 | );
36 |
37 | try {
38 | if (serverInstance) {
39 | if (serverInstance instanceof McpServer) {
40 | logger.info("Closing main MCP server (stdio) instance...", shutdownContext);
41 | await serverInstance.close();
42 | logger.info(
43 | "Main MCP server (stdio) instance closed successfully.",
44 | shutdownContext,
45 | );
46 | } else if (serverInstance instanceof http.Server) {
47 | logger.info("Closing HTTP server instance...", shutdownContext);
48 | const currentHttpServer = serverInstance;
49 | await new Promise<void>((resolve, reject) => {
50 | currentHttpServer.close((err?: Error) => {
51 | if (err) {
52 | logger.error("Error closing HTTP server", err, shutdownContext);
53 | return reject(err);
54 | }
55 | logger.info("HTTP server instance closed successfully.", shutdownContext);
56 | resolve();
57 | });
58 | });
59 | }
60 | } else {
61 | logger.info(
62 | "No global MCP server instance to close (expected for HTTP transport or if not yet initialized).",
63 | shutdownContext,
64 | );
65 | }
66 |
67 | logger.info("Closing Neo4j driver connection...", shutdownContext);
68 | await closeNeo4jConnection();
69 | logger.info(
70 | "Neo4j driver connection closed successfully.",
71 | shutdownContext,
72 | );
73 |
74 | logger.info(
75 | `Graceful shutdown for ${config.mcpServerName} completed successfully. Exiting.`,
76 | shutdownContext,
77 | );
78 | process.exit(0);
79 | } catch (error) {
80 | // Pass the error object directly as the second argument to logger.error
81 | logger.error("Critical error during shutdown", error as Error, {
82 | ...shutdownContext,
83 | // error message and stack are now part of the Error object passed to logger
84 | });
85 | process.exit(1);
86 | }
87 | };
88 |
89 | /**
90 | * Initializes and starts the main MCP server.
91 | * Sets up logging, request context, initializes the server instance, starts the transport,
92 | * and registers signal handlers for graceful shutdown and error handling.
93 | */
94 | const start = async () => {
95 | // --- Logger Initialization ---
96 | const validMcpLogLevels: McpLogLevel[] = [
97 | "debug",
98 | "info",
99 | "notice",
100 | "warning",
101 | "error",
102 | "crit",
103 | "alert",
104 | "emerg",
105 | ];
106 | const initialLogLevelConfig = config.logLevel;
107 | let validatedMcpLogLevel: McpLogLevel = "info"; // Default
108 |
109 | if (validMcpLogLevels.includes(initialLogLevelConfig as McpLogLevel)) {
110 | validatedMcpLogLevel = initialLogLevelConfig as McpLogLevel;
111 | } else {
112 | // Use console.warn here as logger isn't fully initialized yet, only if TTY
113 | if (process.stdout.isTTY) {
114 | console.warn(
115 | `Invalid MCP_LOG_LEVEL "${initialLogLevelConfig}" provided via config/env. Defaulting to "info".`,
116 | );
117 | }
118 | }
119 | // Initialize the logger with the validated MCP level and wait for it to complete.
120 | await logger.initialize(validatedMcpLogLevel);
121 | // The logger.initialize() method itself logs its status, so no redundant log here.
122 | // --- End Logger Initialization ---
123 |
124 | const configLoadContext = requestContextService.createRequestContext({
125 | operation: "ConfigLoad",
126 | });
127 | logger.debug("Configuration loaded successfully", {
128 | ...configLoadContext,
129 | configValues: config,
130 | }); // Spread context and add specific data
131 |
132 | const transportType = config.mcpTransportType;
133 | const startupContext = requestContextService.createRequestContext({
134 | operation: `AtlasServerStartup_${transportType}`,
135 | appName: config.mcpServerName,
136 | appVersion: config.mcpServerVersion,
137 | environment: environment,
138 | });
139 |
140 | logger.info(
141 | `Starting ${config.mcpServerName} v${config.mcpServerVersion} (Transport: ${transportType})...`,
142 | startupContext,
143 | );
144 |
145 | try {
146 | logger.debug(
147 | "Initializing and starting MCP server transport...",
148 | startupContext,
149 | );
150 |
151 | const potentialServer = await initializeAndStartServer();
152 |
153 | if (transportType === "stdio" && potentialServer instanceof McpServer) {
154 | serverInstance = potentialServer;
155 | logger.debug(
156 | "Stored McpServer instance for stdio transport.",
157 | startupContext,
158 | );
159 | } else if (transportType === "http" && potentialServer instanceof http.Server) {
160 | serverInstance = potentialServer;
161 | logger.debug(
162 | "Stored HTTP server instance. MCP sessions are per-request.",
163 | startupContext,
164 | );
165 | } else if (transportType === "http" && !potentialServer) {
166 | logger.debug(
167 | "HTTP transport started. Server instance not returned by initializeAndStartServer. MCP sessions are per-request.",
168 | startupContext,
169 | );
170 | }
171 |
172 |
173 | logger.info(
174 | `${config.mcpServerName} is running with ${transportType} transport.`,
175 | {
176 | ...startupContext,
177 | startTime: new Date().toISOString(),
178 | },
179 | );
180 |
181 | // --- Signal and Error Handling Setup ---
182 | process.on("SIGTERM", () => shutdown("SIGTERM"));
183 | process.on("SIGINT", () => shutdown("SIGINT"));
184 |
185 | process.on("uncaughtException", async (error) => {
186 | const errorContext = {
187 | ...startupContext,
188 | event: "uncaughtException",
189 | error: error instanceof Error ? error.message : String(error),
190 | stack: error instanceof Error ? error.stack : undefined,
191 | };
192 | logger.error(
193 | "Uncaught exception detected. Initiating shutdown...",
194 | errorContext,
195 | );
196 | await shutdown("uncaughtException");
197 | });
198 |
199 | process.on("unhandledRejection", async (reason: unknown) => {
200 | const rejectionContext = {
201 | ...startupContext,
202 | event: "unhandledRejection",
203 | reason: reason instanceof Error ? reason.message : String(reason),
204 | stack: reason instanceof Error ? reason.stack : undefined,
205 | };
206 | logger.error(
207 | "Unhandled promise rejection detected. Initiating shutdown...",
208 | rejectionContext,
209 | );
210 | await shutdown("unhandledRejection");
211 | });
212 | } catch (error) {
213 | logger.error("Critical error during ATLAS MCP Server startup, exiting.", {
214 | ...startupContext,
215 | finalErrorContext: "Startup Failure",
216 | error: error instanceof Error ? error.message : String(error),
217 | stack: error instanceof Error ? error.stack : undefined,
218 | });
219 | process.exit(1);
220 | }
221 | };
222 |
223 | // --- Async IIFE to allow top-level await ---
224 | (async () => {
225 | await start();
226 | })();
227 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_create/createProject.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ProjectService } from "../../../services/neo4j/projectService.js";
2 | import { ProjectDependencyType } from "../../../services/neo4j/types.js"; // Import the enum
3 | import {
4 | BaseErrorCode,
5 | McpError,
6 | ProjectErrorCode,
7 | } from "../../../types/errors.js";
8 | import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
9 | import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
10 | import { ToolContext } from "../../../types/tool.js";
11 | import { AtlasProjectCreateInput, AtlasProjectCreateSchema } from "./types.js";
12 | import { formatProjectCreateResponse } from "./responseFormat.js";
13 |
14 | export const atlasCreateProject = async (
15 | input: unknown,
16 | context: ToolContext,
17 | ) => {
18 | let validatedInput: AtlasProjectCreateInput | undefined;
19 | const reqContext =
20 | context.requestContext ??
21 | requestContextService.createRequestContext({
22 | toolName: "atlasCreateProject",
23 | });
24 |
25 | try {
26 | // Parse and validate input against schema
27 | validatedInput = AtlasProjectCreateSchema.parse(input);
28 |
29 | // Handle single vs bulk project creation based on mode
30 | if (validatedInput.mode === "bulk") {
31 | // Execute bulk creation operation
32 | logger.info("Initializing multiple projects", {
33 | ...reqContext,
34 | count: validatedInput.projects.length,
35 | });
36 |
37 | const results = {
38 | success: true,
39 | message: `Successfully created ${validatedInput.projects.length} projects`,
40 | created: [] as any[],
41 | errors: [] as any[],
42 | };
43 |
44 | // Process each project sequentially to maintain consistency
45 | for (let i = 0; i < validatedInput.projects.length; i++) {
46 | const projectData = validatedInput.projects[i];
47 | try {
48 | const createdProject = await ProjectService.createProject({
49 | name: projectData.name,
50 | description: projectData.description,
51 | status: projectData.status || "active",
52 | urls: projectData.urls || [],
53 | completionRequirements: projectData.completionRequirements,
54 | outputFormat: projectData.outputFormat,
55 | taskType: projectData.taskType,
56 | id: projectData.id, // Use client-provided ID if available
57 | });
58 |
59 | results.created.push(createdProject);
60 |
61 | // Create dependency relationships if specified
62 | if (projectData.dependencies && projectData.dependencies.length > 0) {
63 | for (const dependencyId of projectData.dependencies) {
64 | try {
65 | await ProjectService.addProjectDependency(
66 | createdProject.id,
67 | dependencyId,
68 | ProjectDependencyType.REQUIRES, // Use enum member
69 | "Dependency created during project creation",
70 | );
71 | } catch (error) {
72 | const depErrorContext =
73 | requestContextService.createRequestContext({
74 | ...reqContext,
75 | originalErrorMessage:
76 | error instanceof Error ? error.message : String(error),
77 | originalErrorStack:
78 | error instanceof Error ? error.stack : undefined,
79 | projectId: createdProject.id,
80 | dependencyIdAttempted: dependencyId,
81 | });
82 | logger.warning(
83 | `Failed to create dependency for project ${createdProject.id} to ${dependencyId}`,
84 | depErrorContext,
85 | );
86 | }
87 | }
88 | }
89 | } catch (error) {
90 | results.success = false;
91 | results.errors.push({
92 | index: i,
93 | project: projectData,
94 | error: {
95 | code:
96 | error instanceof McpError
97 | ? error.code
98 | : BaseErrorCode.INTERNAL_ERROR,
99 | message: error instanceof Error ? error.message : "Unknown error",
100 | details: error instanceof McpError ? error.details : undefined,
101 | },
102 | });
103 | }
104 | }
105 |
106 | if (results.errors.length > 0) {
107 | results.message = `Created ${results.created.length} of ${validatedInput.projects.length} projects with ${results.errors.length} errors`;
108 | }
109 |
110 | logger.info("Bulk project initialization completed", {
111 | ...reqContext,
112 | successCount: results.created.length,
113 | errorCount: results.errors.length,
114 | projectIds: results.created.map((p) => p.id),
115 | });
116 |
117 | // Conditionally format response
118 | if (validatedInput.responseFormat === ResponseFormat.JSON) {
119 | return createToolResponse(JSON.stringify(results, null, 2));
120 | } else {
121 | return formatProjectCreateResponse(results);
122 | }
123 | } else {
124 | // Process single project creation
125 | const {
126 | mode,
127 | id,
128 | name,
129 | description,
130 | status,
131 | urls,
132 | completionRequirements,
133 | dependencies,
134 | outputFormat,
135 | taskType,
136 | } = validatedInput;
137 |
138 | logger.info("Initializing new project", {
139 | ...reqContext,
140 | name,
141 | status,
142 | });
143 |
144 | const project = await ProjectService.createProject({
145 | id, // Use client-provided ID if available
146 | name,
147 | description,
148 | status: status || "active",
149 | urls: urls || [],
150 | completionRequirements,
151 | outputFormat,
152 | taskType,
153 | });
154 |
155 | // Create dependency relationships if specified
156 | if (dependencies && dependencies.length > 0) {
157 | for (const dependencyId of dependencies) {
158 | try {
159 | await ProjectService.addProjectDependency(
160 | project.id,
161 | dependencyId,
162 | ProjectDependencyType.REQUIRES, // Use enum member
163 | "Dependency created during project creation",
164 | );
165 | } catch (error) {
166 | const depErrorContext = requestContextService.createRequestContext({
167 | ...reqContext,
168 | originalErrorMessage:
169 | error instanceof Error ? error.message : String(error),
170 | originalErrorStack:
171 | error instanceof Error ? error.stack : undefined,
172 | projectId: project.id,
173 | dependencyIdAttempted: dependencyId,
174 | });
175 | logger.warning(
176 | `Failed to create dependency for project ${project.id} to ${dependencyId}`,
177 | depErrorContext,
178 | );
179 | }
180 | }
181 | }
182 |
183 | logger.info("Project initialized successfully", {
184 | ...reqContext,
185 | projectId: project.id,
186 | });
187 |
188 | // Conditionally format response
189 | if (validatedInput.responseFormat === ResponseFormat.JSON) {
190 | return createToolResponse(JSON.stringify(project, null, 2));
191 | } else {
192 | return formatProjectCreateResponse(project);
193 | }
194 | }
195 | } catch (error) {
196 | // Handle specific error cases
197 | if (error instanceof McpError) {
198 | throw error;
199 | }
200 |
201 | logger.error("Failed to initialize project(s)", error as Error, {
202 | ...reqContext,
203 | inputReceived: validatedInput ?? input, // Log validated or raw input
204 | });
205 |
206 | // Handle duplicate name error specifically
207 | if (error instanceof Error && error.message.includes("duplicate")) {
208 | throw new McpError(
209 | ProjectErrorCode.DUPLICATE_NAME,
210 | `A project with this name already exists`,
211 | {
212 | name:
213 | validatedInput?.mode === "single"
214 | ? validatedInput?.name
215 | : validatedInput?.projects?.[0]?.name,
216 | },
217 | );
218 | }
219 |
220 | // Convert other errors to McpError
221 | throw new McpError(
222 | BaseErrorCode.INTERNAL_ERROR,
223 | `Error creating project(s): ${error instanceof Error ? error.message : "Unknown error"}`,
224 | );
225 | }
226 | };
227 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_update/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import {
3 | McpToolResponse,
4 | ProjectStatus,
5 | ResponseFormat,
6 | TaskType,
7 | createResponseFormatEnum,
8 | } from "../../../types/mcp.js";
9 |
10 | export const ProjectUpdateSchema = z.object({
11 | id: z.string().describe("Identifier of the existing project to be modified"),
12 | updates: z
13 | .object({
14 | name: z
15 | .string()
16 | .min(1)
17 | .max(100)
18 | .optional()
19 | .describe(
20 | "Modified project name following naming conventions (1-100 characters)",
21 | ),
22 | description: z
23 | .string()
24 | .optional()
25 | .describe("Revised project scope, goals, and implementation details"),
26 | status: z
27 | .enum([
28 | ProjectStatus.ACTIVE,
29 | ProjectStatus.PENDING,
30 | ProjectStatus.IN_PROGRESS,
31 | ProjectStatus.COMPLETED,
32 | ProjectStatus.ARCHIVED,
33 | ])
34 | .optional()
35 | .describe(
36 | "Updated lifecycle state reflecting current project progress",
37 | ),
38 | urls: z
39 | .array(
40 | z.object({
41 | title: z.string(),
42 | url: z.string(),
43 | }),
44 | )
45 | .optional()
46 | .describe(
47 | "Modified documentation links, specifications, and technical resources",
48 | ),
49 | completionRequirements: z
50 | .string()
51 | .optional()
52 | .describe(
53 | "Revised definition of done with updated success criteria and metrics",
54 | ),
55 | outputFormat: z
56 | .string()
57 | .optional()
58 | .describe("Modified deliverable specification for project artifacts"),
59 | taskType: z
60 | .enum([
61 | TaskType.RESEARCH,
62 | TaskType.GENERATION,
63 | TaskType.ANALYSIS,
64 | TaskType.INTEGRATION,
65 | ])
66 | .optional()
67 | .describe(
68 | "Revised classification for project categorization and workflow",
69 | ),
70 | })
71 | .describe(
72 | "Partial update object containing only fields that need modification",
73 | ),
74 | });
75 |
76 | const SingleProjectUpdateSchema = z
77 | .object({
78 | mode: z.literal("single"),
79 | id: z.string(),
80 | updates: z.object({
81 | name: z.string().min(1).max(100).optional(),
82 | description: z.string().optional(),
83 | status: z
84 | .enum([
85 | ProjectStatus.ACTIVE,
86 | ProjectStatus.PENDING,
87 | ProjectStatus.IN_PROGRESS,
88 | ProjectStatus.COMPLETED,
89 | ProjectStatus.ARCHIVED,
90 | ])
91 | .optional(),
92 | urls: z
93 | .array(
94 | z.object({
95 | title: z.string(),
96 | url: z.string(),
97 | }),
98 | )
99 | .optional(),
100 | completionRequirements: z.string().optional(),
101 | outputFormat: z.string().optional(),
102 | taskType: z
103 | .enum([
104 | TaskType.RESEARCH,
105 | TaskType.GENERATION,
106 | TaskType.ANALYSIS,
107 | TaskType.INTEGRATION,
108 | ])
109 | .optional(),
110 | }),
111 | responseFormat: createResponseFormatEnum()
112 | .optional()
113 | .default(ResponseFormat.FORMATTED)
114 | .describe(
115 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
116 | ),
117 | })
118 | .describe(
119 | "Atomically update a single project with selective field modifications",
120 | );
121 |
122 | const BulkProjectUpdateSchema = z
123 | .object({
124 | mode: z.literal("bulk"),
125 | projects: z
126 | .array(
127 | z.object({
128 | id: z.string().describe("Identifier of the project to update"),
129 | updates: z.object({
130 | name: z.string().min(1).max(100).optional(),
131 | description: z.string().optional(),
132 | status: z
133 | .enum([
134 | ProjectStatus.ACTIVE,
135 | ProjectStatus.PENDING,
136 | ProjectStatus.IN_PROGRESS,
137 | ProjectStatus.COMPLETED,
138 | ProjectStatus.ARCHIVED,
139 | ])
140 | .optional(),
141 | urls: z
142 | .array(
143 | z.object({
144 | title: z.string(),
145 | url: z.string(),
146 | }),
147 | )
148 | .optional(),
149 | completionRequirements: z.string().optional(),
150 | outputFormat: z.string().optional(),
151 | taskType: z
152 | .enum([
153 | TaskType.RESEARCH,
154 | TaskType.GENERATION,
155 | TaskType.ANALYSIS,
156 | TaskType.INTEGRATION,
157 | ])
158 | .optional(),
159 | }),
160 | }),
161 | )
162 | .min(1)
163 | .max(100)
164 | .describe(
165 | "Collection of project updates to be applied in a single transaction",
166 | ),
167 | responseFormat: createResponseFormatEnum()
168 | .optional()
169 | .default(ResponseFormat.FORMATTED)
170 | .describe(
171 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
172 | ),
173 | })
174 | .describe(
175 | "Update multiple related projects in a single efficient transaction",
176 | );
177 |
178 | // Schema shapes for tool registration
179 | export const AtlasProjectUpdateSchemaShape = {
180 | mode: z
181 | .enum(["single", "bulk"])
182 | .describe(
183 | "Operation mode - 'single' for individual update, 'bulk' for updating multiple projects",
184 | ),
185 | id: z
186 | .string()
187 | .optional()
188 | .describe(
189 | "Project identifier for the update operation (required for mode='single')",
190 | ),
191 | updates: z
192 | .object({
193 | name: z.string().min(1).max(100).optional(),
194 | description: z.string().optional(),
195 | status: z
196 | .enum([
197 | ProjectStatus.ACTIVE,
198 | ProjectStatus.PENDING,
199 | ProjectStatus.IN_PROGRESS,
200 | ProjectStatus.COMPLETED,
201 | ProjectStatus.ARCHIVED,
202 | ])
203 | .optional(),
204 | urls: z
205 | .array(
206 | z.object({
207 | title: z.string(),
208 | url: z.string(),
209 | }),
210 | )
211 | .optional(),
212 | completionRequirements: z.string().optional(),
213 | outputFormat: z.string().optional(),
214 | taskType: z
215 | .enum([
216 | TaskType.RESEARCH,
217 | TaskType.GENERATION,
218 | TaskType.ANALYSIS,
219 | TaskType.INTEGRATION,
220 | ])
221 | .optional(),
222 | })
223 | .optional()
224 | .describe(
225 | "Partial update specifying only the fields to be modified (required for mode='single')",
226 | ),
227 | projects: z
228 | .array(
229 | z.object({
230 | id: z.string(),
231 | updates: z.object({
232 | name: z.string().min(1).max(100).optional(),
233 | description: z.string().optional(),
234 | status: z
235 | .enum([
236 | ProjectStatus.ACTIVE,
237 | ProjectStatus.PENDING,
238 | ProjectStatus.IN_PROGRESS,
239 | ProjectStatus.COMPLETED,
240 | ProjectStatus.ARCHIVED,
241 | ])
242 | .optional(),
243 | urls: z
244 | .array(
245 | z.object({
246 | title: z.string(),
247 | url: z.string(),
248 | }),
249 | )
250 | .optional(),
251 | completionRequirements: z.string().optional(),
252 | outputFormat: z.string().optional(),
253 | taskType: z
254 | .enum([
255 | TaskType.RESEARCH,
256 | TaskType.GENERATION,
257 | TaskType.ANALYSIS,
258 | TaskType.INTEGRATION,
259 | ])
260 | .optional(),
261 | }),
262 | }),
263 | )
264 | .optional()
265 | .describe(
266 | "Collection of project modifications to apply in a single transaction (required for mode='bulk')",
267 | ),
268 | responseFormat: createResponseFormatEnum()
269 | .optional()
270 | .describe(
271 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
272 | ),
273 | } as const;
274 |
275 | // Schema for validation
276 | export const AtlasProjectUpdateSchema = z.discriminatedUnion("mode", [
277 | SingleProjectUpdateSchema,
278 | BulkProjectUpdateSchema,
279 | ]);
280 |
281 | export type AtlasProjectUpdateInput = z.infer<typeof AtlasProjectUpdateSchema>;
282 | export type ProjectUpdateInput = z.infer<typeof ProjectUpdateSchema>;
283 | export type AtlasProjectUpdateResponse = McpToolResponse;
284 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_list/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import {
4 | PriorityLevel,
5 | ResponseFormat,
6 | TaskStatus,
7 | createResponseFormatEnum,
8 | createToolResponse,
9 | } from "../../../types/mcp.js";
10 | import {
11 | createToolExample,
12 | createToolMetadata,
13 | registerTool,
14 | } from "../../../types/tool.js";
15 | import { atlasListTasks } from "./listTasks.js";
16 | import { formatTaskListResponse } from "./responseFormat.js";
17 | import { TaskListRequestInput } from "./types.js"; // Corrected import
18 |
19 | // Schema shapes for tool registration
20 | const TaskListRequestSchemaShape = {
21 | projectId: z
22 | .string()
23 | .describe("ID of the project to list tasks for (required)"),
24 | status: z
25 | .union([
26 | z.enum([
27 | TaskStatus.BACKLOG,
28 | TaskStatus.TODO,
29 | TaskStatus.IN_PROGRESS,
30 | TaskStatus.COMPLETED,
31 | ]),
32 | z.array(
33 | z.enum([
34 | TaskStatus.BACKLOG,
35 | TaskStatus.TODO,
36 | TaskStatus.IN_PROGRESS,
37 | TaskStatus.COMPLETED,
38 | ]),
39 | ),
40 | ])
41 | .optional()
42 | .describe("Filter by task status or array of statuses"),
43 | assignedTo: z.string().optional().describe("Filter by assignment ID"),
44 | priority: z
45 | .union([
46 | z.enum([
47 | PriorityLevel.LOW,
48 | PriorityLevel.MEDIUM,
49 | PriorityLevel.HIGH,
50 | PriorityLevel.CRITICAL,
51 | ]),
52 | z.array(
53 | z.enum([
54 | PriorityLevel.LOW,
55 | PriorityLevel.MEDIUM,
56 | PriorityLevel.HIGH,
57 | PriorityLevel.CRITICAL,
58 | ]),
59 | ),
60 | ])
61 | .optional()
62 | .describe("Filter by priority level or array of priorities"),
63 | tags: z
64 | .array(z.string())
65 | .optional()
66 | .describe(
67 | "Array of tags to filter by (tasks matching any tag will be included)",
68 | ),
69 | taskType: z.string().optional().describe("Filter by task classification"),
70 | sortBy: z
71 | .enum(["priority", "createdAt", "status"])
72 | .optional()
73 | .describe("Field to sort results by (Default: createdAt)"),
74 | sortDirection: z
75 | .enum(["asc", "desc"])
76 | .optional()
77 | .describe("Sort order (Default: desc)"),
78 | page: z
79 | .number()
80 | .optional()
81 | .describe("Page number for paginated results (Default: 1)"),
82 | limit: z
83 | .number()
84 | .optional()
85 | .describe("Number of results per page, maximum 100 (Default: 20)"),
86 | responseFormat: createResponseFormatEnum()
87 | .optional()
88 | .default(ResponseFormat.FORMATTED)
89 | .describe(
90 | "Desired response format: 'formatted' (default string) or 'json' (raw object)",
91 | ),
92 | };
93 |
94 | export const registerAtlasTaskListTool = (server: McpServer) => {
95 | registerTool(
96 | server,
97 | "atlas_task_list",
98 | "Lists tasks according to specified filters with advanced filtering, sorting, and pagination capabilities",
99 | TaskListRequestSchemaShape,
100 | async (input, context) => {
101 | // Parse and process input (assuming validation happens implicitly via registerTool)
102 | const validatedInput = input as unknown as TaskListRequestInput & {
103 | responseFormat?: ResponseFormat;
104 | }; // Corrected type cast
105 | const result = await atlasListTasks(validatedInput, context); // Added context argument
106 |
107 | // Conditionally format response
108 | if (validatedInput.responseFormat === ResponseFormat.JSON) {
109 | // Stringify the result and wrap it in a standard text response
110 | // The client will need to parse this stringified JSON
111 | return createToolResponse(JSON.stringify(result, null, 2));
112 | } else {
113 | // Return the formatted string using the formatter for rich display
114 | return formatTaskListResponse(result, false); // Added second argument
115 | }
116 | },
117 | createToolMetadata({
118 | examples: [
119 | createToolExample(
120 | {
121 | projectId: "proj_example123",
122 | status: "in_progress",
123 | limit: 10,
124 | },
125 | `{
126 | "tasks": [
127 | {
128 | "id": "task_abcd1234",
129 | "projectId": "proj_example123",
130 | "title": "Implement User Authentication",
131 | "description": "Create secure user authentication system with JWT and refresh tokens",
132 | "priority": "high",
133 | "status": "in_progress",
134 | "assignedTo": "user_5678",
135 | "tags": ["security", "backend"],
136 | "completionRequirements": "Authentication endpoints working with proper error handling and tests",
137 | "outputFormat": "Documented API with test coverage",
138 | "taskType": "implementation",
139 | "createdAt": "2025-03-20T14:24:35.123Z",
140 | "updatedAt": "2025-03-22T09:15:22.456Z"
141 | }
142 | ],
143 | "total": 5,
144 | "page": 1,
145 | "limit": 10,
146 | "totalPages": 1
147 | }`,
148 | "List in-progress tasks for a specific project",
149 | ),
150 | createToolExample(
151 | {
152 | projectId: "proj_frontend42",
153 | priority: ["high", "critical"],
154 | tags: ["bug", "urgent"],
155 | sortBy: "priority",
156 | sortDirection: "desc",
157 | },
158 | `{
159 | "tasks": [
160 | {
161 | "id": "task_ef5678",
162 | "projectId": "proj_frontend42",
163 | "title": "Fix Critical UI Rendering Bug",
164 | "description": "Address the UI rendering issue causing layout problems on mobile devices",
165 | "priority": "critical",
166 | "status": "todo",
167 | "tags": ["bug", "ui", "urgent"],
168 | "completionRequirements": "UI displays correctly on all supported mobile devices",
169 | "outputFormat": "Fixed code with browser compatibility tests",
170 | "taskType": "bugfix",
171 | "createdAt": "2025-03-21T10:30:15.789Z",
172 | "updatedAt": "2025-03-21T10:30:15.789Z"
173 | },
174 | {
175 | "id": "task_gh9012",
176 | "projectId": "proj_frontend42",
177 | "title": "Optimize Image Loading Performance",
178 | "description": "Implement lazy loading and optimize image assets to improve page load time",
179 | "priority": "high",
180 | "status": "backlog",
181 | "tags": ["performance", "urgent"],
182 | "completionRequirements": "Page load time reduced by 40% with Lighthouse score above 90",
183 | "outputFormat": "Optimized code with performance benchmarks",
184 | "taskType": "optimization",
185 | "createdAt": "2025-03-19T16:45:22.123Z",
186 | "updatedAt": "2025-03-19T16:45:22.123Z"
187 | }
188 | ],
189 | "total": 2,
190 | "page": 1,
191 | "limit": 20,
192 | "totalPages": 1
193 | }`,
194 | "List high priority and critical tasks with specific tags, sorted by priority",
195 | ),
196 | ],
197 | requiredPermission: "project:read",
198 | returnSchema: z.object({
199 | tasks: z.array(
200 | z.object({
201 | id: z.string(),
202 | projectId: z.string(),
203 | title: z.string(),
204 | description: z.string(),
205 | priority: z.enum([
206 | PriorityLevel.LOW,
207 | PriorityLevel.MEDIUM,
208 | PriorityLevel.HIGH,
209 | PriorityLevel.CRITICAL,
210 | ]),
211 | status: z.enum([
212 | TaskStatus.BACKLOG,
213 | TaskStatus.TODO,
214 | TaskStatus.IN_PROGRESS,
215 | TaskStatus.COMPLETED,
216 | ]),
217 | assignedTo: z.string().optional(),
218 | urls: z
219 | .array(
220 | z.object({
221 | title: z.string(),
222 | url: z.string(),
223 | }),
224 | )
225 | .optional(),
226 | tags: z.array(z.string()).optional(),
227 | completionRequirements: z.string(),
228 | outputFormat: z.string(),
229 | taskType: z.string(),
230 | createdAt: z.string(),
231 | updatedAt: z.string(),
232 | }),
233 | ),
234 | total: z.number().int(),
235 | page: z.number().int(),
236 | limit: z.number().int(),
237 | totalPages: z.number().int(),
238 | }),
239 | rateLimit: {
240 | windowMs: 60 * 1000, // 1 minute
241 | maxRequests: 30, // 30 requests per minute
242 | },
243 | }),
244 | );
245 | };
246 |
```
--------------------------------------------------------------------------------
/scripts/fetch-openapi-spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * @fileoverview Fetches an OpenAPI specification (YAML/JSON) from a URL,
5 | * parses it, and saves it locally in both YAML and JSON formats.
6 | * @module scripts/fetch-openapi-spec
7 | * Includes fallback logic for common OpenAPI file names (openapi.yaml, openapi.json).
8 | * Ensures output paths are within the project directory for security.
9 | *
10 | * @example
11 | * // Fetch spec and save to docs/api/my_api.yaml and docs/api/my_api.json
12 | * // ts-node --esm scripts/fetch-openapi-spec.ts https://api.example.com/v1 docs/api/my_api
13 | *
14 | * @example
15 | * // Fetch spec from a direct file URL
16 | * // ts-node --esm scripts/fetch-openapi-spec.ts https://petstore3.swagger.io/api/v3/openapi.json docs/api/petstore_v3
17 | */
18 |
19 | import axios, { AxiosError } from "axios";
20 | import fs from "fs/promises";
21 | import yaml from "js-yaml";
22 | import path from "path";
23 |
24 | const projectRoot = process.cwd();
25 |
26 | const args = process.argv.slice(2);
27 | const helpFlag = args.includes("--help");
28 | const urlArg = args[0];
29 | const outputBaseArg = args[1];
30 |
31 | if (helpFlag || !urlArg || !outputBaseArg) {
32 | console.log(`
33 | Fetch OpenAPI Specification Script
34 |
35 | Usage:
36 | ts-node --esm scripts/fetch-openapi-spec.ts <url> <output-base-path> [--help]
37 |
38 | Arguments:
39 | <url> Base URL or direct URL to the OpenAPI spec (YAML/JSON).
40 | <output-base-path> Base path for output files (relative to project root),
41 | e.g., 'docs/api/my_api'. Will generate .yaml and .json.
42 | --help Show this help message.
43 |
44 | Example:
45 | ts-node --esm scripts/fetch-openapi-spec.ts https://petstore3.swagger.io/api/v3 docs/api/petstore_v3
46 | `);
47 | process.exit(helpFlag ? 0 : 1);
48 | }
49 |
50 | const outputBasePathAbsolute = path.resolve(projectRoot, outputBaseArg);
51 | const yamlOutputPath = `${outputBasePathAbsolute}.yaml`;
52 | const jsonOutputPath = `${outputBasePathAbsolute}.json`;
53 | const outputDirAbsolute = path.dirname(outputBasePathAbsolute);
54 |
55 | // Security Check: Ensure output paths are within project root
56 | if (
57 | !outputDirAbsolute.startsWith(projectRoot + path.sep) ||
58 | !yamlOutputPath.startsWith(projectRoot + path.sep) ||
59 | !jsonOutputPath.startsWith(projectRoot + path.sep)
60 | ) {
61 | console.error(
62 | `Error: Output path "${outputBaseArg}" resolves outside the project directory. Aborting.`,
63 | );
64 | process.exit(1);
65 | }
66 |
67 | /**
68 | * Attempts to fetch content from a given URL.
69 | * @param url - The URL to fetch data from.
70 | * @returns A promise resolving to an object with data and content type, or null if fetch fails.
71 | */
72 | async function tryFetch(
73 | url: string,
74 | ): Promise<{ data: string; contentType: string | null } | null> {
75 | try {
76 | console.log(`Attempting to fetch from: ${url}`);
77 | const response = await axios.get(url, {
78 | responseType: "text",
79 | validateStatus: (status) => status >= 200 && status < 300,
80 | });
81 | const contentType = response.headers["content-type"] || null;
82 | console.log(
83 | `Successfully fetched (Status: ${response.status}, Content-Type: ${contentType || "N/A"})`,
84 | );
85 | return { data: response.data, contentType };
86 | } catch (error) {
87 | let status = "Unknown";
88 | if (axios.isAxiosError(error)) {
89 | const axiosError = error as AxiosError;
90 | status = axiosError.response
91 | ? String(axiosError.response.status)
92 | : "Network Error";
93 | }
94 | console.warn(`Failed to fetch from ${url} (Status: ${status})`);
95 | return null;
96 | }
97 | }
98 |
99 | /**
100 | * Parses fetched data as YAML or JSON, attempting to infer from content type or by trying both.
101 | * @param data - The raw string data fetched from the URL.
102 | * @param contentType - The content type header from the HTTP response, if available.
103 | * @returns The parsed OpenAPI specification as an object, or null if parsing fails.
104 | */
105 | function parseSpec(data: string, contentType: string | null): object | null {
106 | try {
107 | const lowerContentType = contentType?.toLowerCase();
108 | if (
109 | lowerContentType?.includes("yaml") ||
110 | lowerContentType?.includes("yml")
111 | ) {
112 | console.log("Parsing content as YAML based on Content-Type...");
113 | return yaml.load(data) as object;
114 | } else if (lowerContentType?.includes("json")) {
115 | console.log("Parsing content as JSON based on Content-Type...");
116 | return JSON.parse(data);
117 | } else {
118 | console.log(
119 | "Content-Type is ambiguous or missing. Attempting to parse as YAML first...",
120 | );
121 | try {
122 | const parsedYaml = yaml.load(data) as object;
123 | // Basic validation: check if it's a non-null object.
124 | if (parsedYaml && typeof parsedYaml === "object") {
125 | console.log("Successfully parsed as YAML.");
126 | return parsedYaml;
127 | }
128 | } catch (yamlError) {
129 | console.log("YAML parsing failed. Attempting to parse as JSON...");
130 | try {
131 | const parsedJson = JSON.parse(data);
132 | if (parsedJson && typeof parsedJson === "object") {
133 | console.log("Successfully parsed as JSON.");
134 | return parsedJson;
135 | }
136 | } catch (jsonError) {
137 | console.warn(
138 | "Could not parse content as YAML or JSON after attempting both.",
139 | );
140 | return null;
141 | }
142 | }
143 | // If YAML parsing resulted in a non-object (e.g. string, number) but didn't throw
144 | console.warn(
145 | "Content parsed as YAML but was not a valid object structure. Trying JSON.",
146 | );
147 | try {
148 | const parsedJson = JSON.parse(data);
149 | if (parsedJson && typeof parsedJson === "object") {
150 | console.log(
151 | "Successfully parsed as JSON on second attempt for non-object YAML.",
152 | );
153 | return parsedJson;
154 | }
155 | } catch (jsonError) {
156 | console.warn(
157 | "Could not parse content as YAML or JSON after attempting both.",
158 | );
159 | return null;
160 | }
161 | }
162 | } catch (parseError) {
163 | console.error(
164 | `Error parsing specification: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
165 | );
166 | }
167 | return null;
168 | }
169 |
170 | /**
171 | * Main orchestrator function. Fetches the OpenAPI spec from the provided URL (with fallbacks),
172 | * parses it, and saves it to the specified output paths in both YAML and JSON formats.
173 | */
174 | async function fetchAndProcessSpec(): Promise<void> {
175 | let fetchedResult: { data: string; contentType: string | null } | null = null;
176 | const potentialUrls: string[] = [urlArg];
177 |
178 | if (
179 | !urlArg.endsWith(".yaml") &&
180 | !urlArg.endsWith(".yml") &&
181 | !urlArg.endsWith(".json")
182 | ) {
183 | const urlWithoutTrailingSlash = urlArg.endsWith("/")
184 | ? urlArg.slice(0, -1)
185 | : urlArg;
186 | potentialUrls.push(`${urlWithoutTrailingSlash}/openapi.yaml`);
187 | potentialUrls.push(`${urlWithoutTrailingSlash}/openapi.json`);
188 | }
189 |
190 | for (const url of potentialUrls) {
191 | fetchedResult = await tryFetch(url);
192 | if (fetchedResult) break;
193 | }
194 |
195 | if (!fetchedResult) {
196 | console.error(
197 | `Error: Failed to fetch specification from all attempted URLs: ${potentialUrls.join(", ")}. Aborting.`,
198 | );
199 | process.exit(1);
200 | }
201 |
202 | const openapiSpec = parseSpec(fetchedResult.data, fetchedResult.contentType);
203 |
204 | if (!openapiSpec || typeof openapiSpec !== "object") {
205 | console.error(
206 | "Error: Failed to parse specification content or content is not a valid object. Aborting.",
207 | );
208 | process.exit(1);
209 | }
210 |
211 | try {
212 | await fs.access(outputDirAbsolute);
213 | } catch (error: any) {
214 | if (error.code === "ENOENT") {
215 | console.log(`Output directory not found. Creating: ${outputDirAbsolute}`);
216 | await fs.mkdir(outputDirAbsolute, { recursive: true });
217 | } else {
218 | console.error(
219 | `Error accessing output directory ${outputDirAbsolute}: ${error.message}. Aborting.`,
220 | );
221 | process.exit(1);
222 | }
223 | }
224 |
225 | try {
226 | console.log(`Saving YAML specification to: ${yamlOutputPath}`);
227 | await fs.writeFile(yamlOutputPath, yaml.dump(openapiSpec), "utf8");
228 | console.log(`Successfully saved YAML specification.`);
229 | } catch (error) {
230 | console.error(
231 | `Error saving YAML to ${yamlOutputPath}: ${error instanceof Error ? error.message : String(error)}. Aborting.`,
232 | );
233 | process.exit(1);
234 | }
235 |
236 | try {
237 | console.log(`Saving JSON specification to: ${jsonOutputPath}`);
238 | await fs.writeFile(
239 | jsonOutputPath,
240 | JSON.stringify(openapiSpec, null, 2),
241 | "utf8",
242 | );
243 | console.log(`Successfully saved JSON specification.`);
244 | } catch (error) {
245 | console.error(
246 | `Error saving JSON to ${jsonOutputPath}: ${error instanceof Error ? error.message : String(error)}. Aborting.`,
247 | );
248 | process.exit(1);
249 | }
250 |
251 | console.log("OpenAPI specification processed and saved successfully.");
252 | }
253 |
254 | fetchAndProcessSpec();
255 |
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_update/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import { ProjectStatus, createProjectStatusEnum } from "../../../types/mcp.js";
4 | import {
5 | createToolExample,
6 | createToolMetadata,
7 | registerTool,
8 | } from "../../../types/tool.js";
9 | import { atlasUpdateProject } from "./updateProject.js";
10 | import { AtlasProjectUpdateSchemaShape } from "./types.js";
11 |
12 | export const registerAtlasProjectUpdateTool = (server: McpServer) => {
13 | registerTool(
14 | server,
15 | "atlas_project_update",
16 | "Modifies attributes of existing project entities within the system with support for both targeted single updates and efficient bulk modifications",
17 | AtlasProjectUpdateSchemaShape,
18 | atlasUpdateProject,
19 | createToolMetadata({
20 | examples: [
21 | createToolExample(
22 | {
23 | mode: "single",
24 | id: "proj_ms_migration",
25 | updates: {
26 | name: "Microservice Architecture Migration - Phase 2",
27 | description:
28 | "Extended refactoring to include data migration layer and enhanced service discovery through etcd integration",
29 | status: "in-progress",
30 | },
31 | },
32 | `{
33 | "id": "proj_ms_migration",
34 | "name": "Microservice Architecture Migration - Phase 2",
35 | "description": "Extended refactoring to include data migration layer and enhanced service discovery through etcd integration",
36 | "status": "in-progress",
37 | "urls": [
38 | {"title": "MCP Server Repository", "url": "https://github.com/cyanheads/atlas-mcp-server.git"},
39 | {"title": "Technical Spec", "url": "file:///Users/username/project_name/docs/atlas-reference.md"},
40 | {"title": "MCP Docs", "url": "https://modelcontextprotocol.io/"}
41 | ],
42 | "completionRequirements": "All critical services migrated with 100% test coverage, performance metrics meeting SLAs, and zero regressions in core functionality",
43 | "outputFormat": "Containerized services with CI/CD pipelines, comprehensive API documentation, and migration runbook",
44 | "taskType": "integration",
45 | "createdAt": "2025-03-23T10:11:24.123Z",
46 | "updatedAt": "2025-03-23T10:12:34.456Z"
47 | }`,
48 | "Update project scope and phase for an ongoing engineering initiative",
49 | ),
50 | createToolExample(
51 | {
52 | mode: "bulk",
53 | projects: [
54 | {
55 | id: "proj_graphql",
56 | updates: {
57 | status: "completed",
58 | completionRequirements:
59 | "API supports all current use cases with n+1 query optimization, proper error handling, and 95% test coverage with performance benchmarks showing 30% reduction in API request times",
60 | },
61 | },
62 | {
63 | id: "proj_perf",
64 | updates: {
65 | status: "in-progress",
66 | description:
67 | "Extended performance analysis to include bundle size optimization, lazy-loading routes, and server-side rendering for critical pages",
68 | urls: [
69 | {
70 | title: "Lighthouse CI Results",
71 | url: "https://lighthouse-ci.app/dashboard?project=frontend-perf",
72 | },
73 | {
74 | title: "Web Vitals Tracking",
75 | url: "https://analytics.google.com/web-vitals",
76 | },
77 | ],
78 | },
79 | },
80 | ],
81 | },
82 | `{
83 | "success": true,
84 | "message": "Successfully updated 2 projects",
85 | "updated": [
86 | {
87 | "id": "proj_graphql",
88 | "name": "GraphQL API Implementation",
89 | "description": "Design and implement GraphQL API layer to replace existing REST endpoints with optimized query capabilities",
90 | "status": "completed",
91 | "urls": [],
92 | "completionRequirements": "API supports all current use cases with n+1 query optimization, proper error handling, and 95% test coverage with performance benchmarks showing 30% reduction in API request times",
93 | "outputFormat": "TypeScript-based GraphQL schema with resolvers, documentation, and integration tests",
94 | "taskType": "generation",
95 | "createdAt": "2025-03-23T10:11:24.123Z",
96 | "updatedAt": "2025-03-23T10:12:34.456Z"
97 | },
98 | {
99 | "id": "proj_perf",
100 | "name": "Performance Optimization Suite",
101 | "description": "Extended performance analysis to include bundle size optimization, lazy-loading routes, and server-side rendering for critical pages",
102 | "status": "in-progress",
103 | "urls": [
104 | {"title": "Lighthouse CI Results", "url": "https://lighthouse-ci.app/dashboard?project=frontend-perf"},
105 | {"title": "Web Vitals Tracking", "url": "https://analytics.google.com/web-vitals"}
106 | ],
107 | "completionRequirements": "Core React components meet Web Vitals thresholds with 50% reduction in LCP and TTI metrics",
108 | "outputFormat": "Optimized component library, performance test suite, and technical recommendation document",
109 | "taskType": "analysis",
110 | "createdAt": "2025-03-23T10:11:24.456Z",
111 | "updatedAt": "2025-03-23T10:12:34.789Z"
112 | }
113 | ],
114 | "errors": []
115 | }`,
116 | "Synchronize project statuses across dependent engineering initiatives",
117 | ),
118 | ],
119 | requiredPermission: "project:update",
120 | returnSchema: z.union([
121 | // Single project response
122 | z.object({
123 | id: z.string().describe("Project ID"),
124 | name: z.string().describe("Project name"),
125 | description: z.string().describe("Project description"),
126 | status: createProjectStatusEnum().describe("Project status"),
127 | urls: z
128 | .array(
129 | z.object({
130 | title: z.string(),
131 | url: z.string(),
132 | }),
133 | )
134 | .describe("Reference materials"),
135 | completionRequirements: z.string().describe("Completion criteria"),
136 | outputFormat: z.string().describe("Deliverable format"),
137 | taskType: z.string().describe("Project classification"),
138 | createdAt: z.string().describe("Creation timestamp"),
139 | updatedAt: z.string().describe("Last update timestamp"),
140 | }),
141 | // Bulk update response
142 | z.object({
143 | success: z.boolean().describe("Operation success status"),
144 | message: z.string().describe("Result message"),
145 | updated: z
146 | .array(
147 | z.object({
148 | id: z.string().describe("Project ID"),
149 | name: z.string().describe("Project name"),
150 | description: z.string().describe("Project description"),
151 | status: createProjectStatusEnum().describe("Project status"),
152 | urls: z
153 | .array(
154 | z.object({
155 | title: z.string(),
156 | url: z.string(),
157 | }),
158 | )
159 | .describe("Reference materials"),
160 | completionRequirements: z
161 | .string()
162 | .describe("Completion criteria"),
163 | outputFormat: z.string().describe("Deliverable format"),
164 | taskType: z.string().describe("Project classification"),
165 | createdAt: z.string().describe("Creation timestamp"),
166 | updatedAt: z.string().describe("Last update timestamp"),
167 | }),
168 | )
169 | .describe("Updated projects"),
170 | errors: z
171 | .array(
172 | z.object({
173 | index: z.number().describe("Index in the projects array"),
174 | project: z.any().describe("Original project update data"),
175 | error: z
176 | .object({
177 | code: z.string().describe("Error code"),
178 | message: z.string().describe("Error message"),
179 | details: z
180 | .any()
181 | .optional()
182 | .describe("Additional error details"),
183 | })
184 | .describe("Error information"),
185 | }),
186 | )
187 | .describe("Update errors"),
188 | }),
189 | ]),
190 | rateLimit: {
191 | windowMs: 60 * 1000, // 1 minute
192 | maxRequests: 15, // 15 requests per minute (either single or bulk)
193 | },
194 | }),
195 | );
196 | };
197 |
```
--------------------------------------------------------------------------------
/src/webui/styling/components.css:
--------------------------------------------------------------------------------
```css
1 | /* ==========================================================================
2 | Component Styles: Buttons, Selects, Cards, Toggles etc.
3 | ========================================================================== */
4 | select,
5 | button {
6 | padding: var(--spacing-sm) var(--spacing-md);
7 | border-radius: var(--border-radius-md);
8 | border: 1px solid var(--border-color);
9 | font-size: 1rem;
10 | background-color: var(--card-bg-color); /* Match card for consistency */
11 | color: var(--text-color);
12 | transition:
13 | background-color 0.15s ease-out,
14 | border-color 0.15s ease-out,
15 | box-shadow 0.15s ease-out;
16 | }
17 |
18 | select:focus,
19 | button:focus {
20 | outline: none;
21 | border-color: var(--primary-color);
22 | box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25); /* Standard Bootstrap-like focus ring */
23 | }
24 | /* For dark mode, ensure focus ring is visible */
25 | .dark-mode select:focus,
26 | .dark-mode button:focus {
27 | box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.35);
28 | }
29 |
30 | select {
31 | flex-grow: 1;
32 | min-width: 200px; /* Ensure select has a minimum width */
33 | }
34 |
35 | button {
36 | background-color: var(--primary-color);
37 | color: var(--button-text-color);
38 | cursor: pointer;
39 | font-weight: 500;
40 | border-color: var(--primary-color);
41 | }
42 |
43 | button:hover {
44 | background-color: var(--primary-hover-color);
45 | border-color: var(--primary-hover-color);
46 | }
47 |
48 | button#refresh-button {
49 | background-color: var(--button-secondary-bg);
50 | border-color: var(--button-secondary-bg);
51 | color: var(--button-text-color);
52 | }
53 | button#refresh-button:hover {
54 | background-color: var(--button-secondary-hover-bg);
55 | border-color: var(--button-secondary-hover-bg);
56 | }
57 |
58 | .view-controls {
59 | display: flex;
60 | gap: var(--spacing-sm);
61 | }
62 |
63 | .view-controls .view-toggle-button {
64 | padding: var(--spacing-xs) var(--spacing-sm);
65 | font-size: 0.85rem;
66 | background-color: transparent;
67 | color: var(--primary-color);
68 | border: 1px solid var(--primary-color);
69 | }
70 | .view-controls .view-toggle-button:hover,
71 | .view-controls .view-toggle-button:focus {
72 | background-color: rgba(
73 | 0,
74 | 123,
75 | 255,
76 | 0.1
77 | ); /* Use primary color with alpha for hover */
78 | }
79 | .dark-mode .view-controls .view-toggle-button:hover,
80 | .dark-mode .view-controls .view-toggle-button:focus {
81 | background-color: rgba(
82 | 0,
83 | 123,
84 | 255,
85 | 0.2
86 | ); /* Slightly more opaque for dark mode */
87 | }
88 |
89 | /* Compact View for Tasks/Knowledge */
90 | .data-item.compact {
91 | padding: var(--spacing-sm) 0;
92 | display: flex;
93 | justify-content: space-between;
94 | align-items: center;
95 | }
96 | .data-item.compact strong {
97 | margin-bottom: 0;
98 | font-weight: 500;
99 | }
100 | .data-item.compact .item-status {
101 | font-size: 0.85rem;
102 | color: var(--secondary-text-color);
103 | background-color: var(--code-bg-color);
104 | padding: 2px 6px;
105 | border-radius: var(--border-radius-sm);
106 | }
107 | /* Hide detailed elements in compact mode */
108 | .data-item.compact pre,
109 | .data-item.compact div:not(:first-child), /* Hides divs other than the one containing the title/status */
110 | .data-item.compact ul {
111 | display: none;
112 | }
113 |
114 | .task-card {
115 | background-color: var(--card-bg-color);
116 | padding: var(--spacing-sm);
117 | border-radius: var(--border-radius-sm);
118 | border: 1px solid var(--data-item-border-color);
119 | box-shadow: 0 2px 4px var(--shadow-color);
120 | cursor: pointer; /* If tasks are clickable for details */
121 | transition:
122 | box-shadow 0.15s ease-out,
123 | transform 0.15s ease-out;
124 | }
125 |
126 | .task-card:hover {
127 | box-shadow: 0 4px 8px var(--shadow-color);
128 | transform: translateY(-2px);
129 | }
130 |
131 | .task-card-title {
132 | font-weight: 600;
133 | margin-bottom: var(--spacing-xs);
134 | color: var(--text-color);
135 | }
136 |
137 | .task-card-priority,
138 | .task-card-assignee {
139 | font-size: 0.8rem;
140 | color: var(--secondary-text-color);
141 | margin-top: var(--spacing-xs);
142 | }
143 |
144 | .empty-column {
145 | text-align: center;
146 | color: var(--secondary-text-color);
147 | font-style: italic;
148 | padding: var(--spacing-md) 0;
149 | }
150 |
151 | .explorer-node-item {
152 | padding: var(--spacing-sm);
153 | border-bottom: 1px solid var(--data-item-border-color);
154 | cursor: pointer;
155 | transition: background-color 0.15s ease-out;
156 | }
157 |
158 | .explorer-node-item:last-child {
159 | border-bottom: none;
160 | }
161 |
162 | .explorer-node-item:hover {
163 | background-color: var(--bg-color); /* Subtle hover effect */
164 | }
165 | .dark-mode .explorer-node-item:hover {
166 | background-color: var(--border-color); /* Darker hover for dark mode */
167 | }
168 |
169 | /* ==========================================================================
170 | Status, Error, Loading Messages
171 | ========================================================================== */
172 | .error {
173 | /* For #error-message div */
174 | color: var(--error-color);
175 | font-weight: 500;
176 | padding: var(--spacing-sm) var(--spacing-md);
177 | background-color: var(--error-bg-color);
178 | border: 1px solid var(--error-border-color);
179 | border-radius: var(--border-radius-sm);
180 | margin-top: var(--spacing-md);
181 | transition:
182 | background-color 0.2s ease-out,
183 | border-color 0.2s ease-out,
184 | color 0.2s ease-out;
185 | }
186 |
187 | .loading {
188 | /* For loading text within containers */
189 | font-style: italic;
190 | color: var(--secondary-text-color);
191 | padding: var(--spacing-md) 0;
192 | text-align: center;
193 | }
194 |
195 | #connection-status {
196 | margin-top: var(
197 | --spacing-lg
198 | ); /* Ensure it's below error message if both visible */
199 | padding: var(--spacing-sm) var(--spacing-md);
200 | background-color: var(--connection-status-bg);
201 | border-radius: var(--border-radius-sm);
202 | text-align: center;
203 | font-weight: 500;
204 | color: var(--text-color);
205 | transition:
206 | background-color 0.2s ease-out,
207 | color 0.2s ease-out;
208 | }
209 | #connection-status span {
210 | /* The actual status text, e.g., "Connected" */
211 | font-weight: 600;
212 | }
213 |
214 | /* ==========================================================================
215 | Utility Classes
216 | ========================================================================== */
217 | .hidden {
218 | display: none !important;
219 | }
220 |
221 | /* ==========================================================================
222 | Theme Toggle Switch
223 | ========================================================================== */
224 | .theme-switch-wrapper {
225 | display: flex;
226 | align-items: center;
227 | position: absolute;
228 | top: var(--spacing-md);
229 | right: var(--spacing-md);
230 | gap: var(--spacing-sm);
231 | }
232 | .theme-label {
233 | font-size: 0.8rem;
234 | color: var(--secondary-text-color);
235 | cursor: pointer;
236 | }
237 |
238 | .theme-switch {
239 | /* The label acting as a container */
240 | display: inline-block;
241 | height: 20px; /* Smaller toggle */
242 | position: relative;
243 | width: 40px; /* Smaller toggle */
244 | }
245 | .theme-switch input {
246 | display: none;
247 | } /* Hide actual checkbox */
248 |
249 | .slider {
250 | /* The visual track of the switch */
251 | background-color: #ccc; /* Default off state */
252 | position: absolute;
253 | cursor: pointer;
254 | top: 0;
255 | left: 0;
256 | right: 0;
257 | bottom: 0;
258 | transition: 0.3s;
259 | border-radius: 20px; /* Fully rounded ends */
260 | }
261 | .slider:before {
262 | /* The circular handle */
263 | background-color: #fff;
264 | position: absolute;
265 | content: "";
266 | height: 14px; /* Smaller handle */
267 | width: 14px; /* Smaller handle */
268 | left: 3px; /* Padding from left edge */
269 | bottom: 3px; /* Padding from bottom edge */
270 | transition: 0.3s;
271 | border-radius: 50%; /* Circular */
272 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
273 | }
274 |
275 | input:checked + .slider {
276 | background-color: var(--primary-color);
277 | } /* "On" state color */
278 | input:checked + .slider:before {
279 | transform: translateX(20px);
280 | } /* Move handle to the right */
281 |
282 | /* ==========================================================================
283 | Responsive Adjustments
284 | ========================================================================== */
285 | @media (max-width: 768px) {
286 | body {
287 | font-size: 15px;
288 | }
289 | #app {
290 | margin: var(--spacing-md);
291 | padding: var(--spacing-md);
292 | }
293 | h1 {
294 | font-size: 1.8rem;
295 | }
296 | h2 {
297 | font-size: 1.5rem;
298 | }
299 | h3 {
300 | font-size: 1.2rem;
301 | }
302 |
303 | .controls-container {
304 | flex-direction: column;
305 | align-items: stretch; /* Make controls full width */
306 | }
307 | .controls-container select,
308 | .controls-container button {
309 | width: 100%;
310 | margin-bottom: var(--spacing-sm); /* Consistent spacing when stacked */
311 | }
312 | .controls-container button:last-child {
313 | margin-bottom: 0;
314 | }
315 |
316 | .section-header {
317 | flex-direction: column;
318 | align-items: flex-start; /* Align header content to the start */
319 | }
320 | .view-controls {
321 | width: 100%;
322 | justify-content: flex-start; /* Align toggles to start */
323 | }
324 |
325 | .theme-switch-wrapper {
326 | top: var(--spacing-sm);
327 | right: var(--spacing-sm);
328 | }
329 | .theme-label {
330 | display: none; /* Hide "Toggle Theme" text on small screens to save space */
331 | }
332 |
333 | #details-content.details-grid {
334 | grid-template-columns: 1fr; /* Stack labels and values */
335 | }
336 | #details-content.details-grid > .data-item > strong {
337 | /* Label */
338 | margin-bottom: var(
339 | --spacing-xs
340 | ); /* Space between label and value when stacked */
341 | grid-column: 1; /* Ensure it stays in the first column */
342 | }
343 | #details-content.details-grid > .data-item > div,
344 | #details-content.details-grid > .data-item > pre,
345 | #details-content.details-grid > .data-item > ul {
346 | /* Value */
347 | grid-column: 1; /* Ensure it stays in the first column */
348 | }
349 | }
350 |
```