@mtaap/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +864 -0
- package/dist/permissions.d.ts +7 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +25 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# MTAAP MCP Server
|
|
2
|
+
|
|
3
|
+
Standalone MCP server for managing tasks, projects, and git integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install globally
|
|
9
|
+
npm install -g @mtaap/mcp
|
|
10
|
+
|
|
11
|
+
# Or run directly
|
|
12
|
+
npx @mtaap/mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
### Required Environment Variables
|
|
18
|
+
|
|
19
|
+
| Variable | Description |
|
|
20
|
+
|----------|-------------|
|
|
21
|
+
| `MTAAP_API_KEY` | Your MTAAP API key. Generate one at https://app.mtaap.io/settings/api-keys |
|
|
22
|
+
|
|
23
|
+
### Optional Environment Variables
|
|
24
|
+
|
|
25
|
+
| Variable | Description |
|
|
26
|
+
|----------|-------------|
|
|
27
|
+
| `GITHUB_TOKEN` | GitHub personal access token for branch creation and PR features |
|
|
28
|
+
|
|
29
|
+
### Example
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export MTAAP_API_KEY=usr_xxxxx...
|
|
33
|
+
export GITHUB_TOKEN=ghp_xxxxx... # Optional, for git operations
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Available Tools
|
|
37
|
+
|
|
38
|
+
### list_projects
|
|
39
|
+
List accessible projects (personal + team via tags).
|
|
40
|
+
|
|
41
|
+
### list_tasks
|
|
42
|
+
Returns available tasks (filterable by project, state).
|
|
43
|
+
|
|
44
|
+
### get_task
|
|
45
|
+
Full task details including acceptance criteria.
|
|
46
|
+
|
|
47
|
+
### assign_task
|
|
48
|
+
Atomic claim → creates branch. Fails if already taken.
|
|
49
|
+
|
|
50
|
+
### update_progress
|
|
51
|
+
Reports status, updates checkboxes, writes checkpoint.
|
|
52
|
+
|
|
53
|
+
### complete_task
|
|
54
|
+
Marks complete, triggers PR, deletes local state file.
|
|
55
|
+
|
|
56
|
+
### check_active_task
|
|
57
|
+
Check for resumable task in `.mtaap/active-task.json`.
|
|
58
|
+
|
|
59
|
+
### report_error
|
|
60
|
+
Report unrecoverable error, displays on task in webapp.
|
|
61
|
+
|
|
62
|
+
### get_project_context
|
|
63
|
+
Returns assembled context (README, stack, conventions).
|
|
64
|
+
|
|
65
|
+
### add_note
|
|
66
|
+
Append implementation notes to task.
|
|
67
|
+
|
|
68
|
+
### abandon_task
|
|
69
|
+
Unassign self, cleanup branch, return to ready state.
|
|
70
|
+
|
|
71
|
+
### create_personal_project
|
|
72
|
+
Create project in user's personal workspace.
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
1. Configure your environment variables
|
|
77
|
+
2. Add to your MCP client configuration (Claude Desktop, OpenCode, etc.):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"mtaap": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["@mtaap/mcp"],
|
|
85
|
+
"env": {
|
|
86
|
+
"MTAAP_BASE_URL": "https://app.mtaap.io",
|
|
87
|
+
"MTAAP_API_KEY": "your-api-key"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
3. Use the tools from your AI agent
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Build
|
|
100
|
+
pnpm build
|
|
101
|
+
|
|
102
|
+
# Watch mode
|
|
103
|
+
pnpm dev
|
|
104
|
+
|
|
105
|
+
# Run locally
|
|
106
|
+
MTAAP_API_KEY=your-key pnpm start
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Troubleshooting
|
|
110
|
+
|
|
111
|
+
### "MTAAP_API_KEY is required"
|
|
112
|
+
|
|
113
|
+
You need to set the `MTAAP_API_KEY` environment variable. Generate an API key at https://app.mtaap.io/settings/api-keys
|
|
114
|
+
|
|
115
|
+
### "GITHUB_TOKEN is not set" warning
|
|
116
|
+
|
|
117
|
+
This warning appears when `GITHUB_TOKEN` is not configured. Git operations (branch creation, PR creation) require this token. The server will still start, but git-related features will fail.
|
|
118
|
+
|
|
119
|
+
### "Invalid API key"
|
|
120
|
+
|
|
121
|
+
Your API key may be expired or revoked. Generate a new one at https://app.mtaap.io/settings/api-keys
|
|
122
|
+
|
|
123
|
+
### Connection issues
|
|
124
|
+
|
|
125
|
+
Ensure your firewall allows outbound connections to your MTAAP instance. By default, the server connects to `https://app.mtaap.io`.
|
|
126
|
+
|
|
127
|
+
## Publishing
|
|
128
|
+
|
|
129
|
+
This package is published as `@mtaap/mcp` to npm.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Build and publish
|
|
133
|
+
pnpm build
|
|
134
|
+
npm publish
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createMCPServer } from "./index.js";
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for the MTAAP MCP server.
|
|
5
|
+
* Validates environment, starts the server, and handles graceful shutdown.
|
|
6
|
+
*/
|
|
7
|
+
function validateEnvironment() {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
// Required: MTAAP_API_KEY
|
|
11
|
+
if (!process.env.MTAAP_API_KEY) {
|
|
12
|
+
errors.push("MTAAP_API_KEY is required. Generate one at https://app.mtaap.io/settings/api-keys");
|
|
13
|
+
}
|
|
14
|
+
// Optional but recommended: GITHUB_TOKEN
|
|
15
|
+
if (!process.env.GITHUB_TOKEN) {
|
|
16
|
+
warnings.push("GITHUB_TOKEN is not set. Branch creation and PR features will not work.");
|
|
17
|
+
}
|
|
18
|
+
// Print warnings
|
|
19
|
+
for (const warning of warnings) {
|
|
20
|
+
console.error(`[mtaap-mcp] Warning: ${warning}`);
|
|
21
|
+
}
|
|
22
|
+
// Exit if there are errors
|
|
23
|
+
if (errors.length > 0) {
|
|
24
|
+
for (const error of errors) {
|
|
25
|
+
console.error(`[mtaap-mcp] Error: ${error}`);
|
|
26
|
+
}
|
|
27
|
+
console.error("\nRequired environment variables:");
|
|
28
|
+
console.error(" MTAAP_API_KEY Your MTAAP API key");
|
|
29
|
+
console.error("\nOptional environment variables:");
|
|
30
|
+
console.error(" GITHUB_TOKEN GitHub personal access token for git operations");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
console.error("[mtaap-mcp] Starting MTAAP MCP server...");
|
|
36
|
+
validateEnvironment();
|
|
37
|
+
try {
|
|
38
|
+
await createMCPServer();
|
|
39
|
+
console.error("[mtaap-mcp] Server started successfully");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
console.error(`[mtaap-mcp] Failed to start server: ${message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Graceful shutdown handling
|
|
48
|
+
function setupShutdownHandlers() {
|
|
49
|
+
const shutdown = (signal) => {
|
|
50
|
+
console.error(`[mtaap-mcp] Received ${signal}, shutting down...`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
54
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
55
|
+
// Handle uncaught exceptions
|
|
56
|
+
process.on("uncaughtException", (error) => {
|
|
57
|
+
console.error("[mtaap-mcp] Uncaught exception:", error.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
// Handle unhandled promise rejections
|
|
61
|
+
process.on("unhandledRejection", (reason) => {
|
|
62
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
63
|
+
console.error("[mtaap-mcp] Unhandled rejection:", message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
setupShutdownHandlers();
|
|
68
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+IA,wBAAsB,eAAe,kBAiZpC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { validateApiKey } from "@mtaap/auth";
|
|
4
|
+
import { prisma } from "@mtaap/db";
|
|
5
|
+
import { TaskState, ProjectType, ProjectOrigin, VERSION, } from "@mtaap/core";
|
|
6
|
+
import { ApiKeyPermission } from "@mtaap/core";
|
|
7
|
+
import { GitHubProvider } from "@mtaap/git";
|
|
8
|
+
import { assertApiKeyPermission } from "./permissions.js";
|
|
9
|
+
import { ListProjectsInputSchema, ListTasksInputSchema, GetTaskInputSchema, AssignTaskInputSchema, UpdateProgressInputSchema, CompleteTaskInputSchema, ReportErrorInputSchema, GetProjectContextInputSchema, AddNoteInputSchema, AbandonTaskInputSchema, CreatePersonalProjectInputSchema, } from "@mtaap/core";
|
|
10
|
+
async function getAuthenticatedUser(apiKey) {
|
|
11
|
+
const result = await validateApiKey(apiKey);
|
|
12
|
+
if (!result) {
|
|
13
|
+
throw new Error("Invalid API key");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
userId: result.user.id,
|
|
17
|
+
organizationId: result.organization?.id,
|
|
18
|
+
apiKey: result.apiKey,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function ensureAccessControl(projectId, userId, organizationId) {
|
|
22
|
+
if (!organizationId) {
|
|
23
|
+
const project = await prisma.project.findUnique({
|
|
24
|
+
where: { id: projectId },
|
|
25
|
+
include: { owner: true },
|
|
26
|
+
});
|
|
27
|
+
if (project?.ownerId !== userId) {
|
|
28
|
+
throw new Error("Access denied: You do not have permission to access this project");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const project = await prisma.project.findUnique({
|
|
33
|
+
where: { id: projectId },
|
|
34
|
+
include: { organization: true },
|
|
35
|
+
});
|
|
36
|
+
if (!project || project.organizationId !== organizationId) {
|
|
37
|
+
throw new Error("Access denied: You do not have permission to access this project");
|
|
38
|
+
}
|
|
39
|
+
const userTags = await prisma.userTag.findMany({
|
|
40
|
+
where: { userId },
|
|
41
|
+
include: { tag: true },
|
|
42
|
+
});
|
|
43
|
+
const projectTags = await prisma.projectTag.findMany({
|
|
44
|
+
where: { projectId },
|
|
45
|
+
include: { tag: true },
|
|
46
|
+
});
|
|
47
|
+
const userTagNames = new Set(userTags.map((ut) => ut.tag.name));
|
|
48
|
+
const projectTagNames = new Set(projectTags.map((pt) => pt.tag.name));
|
|
49
|
+
const hasAccess = [...userTagNames].some((tag) => projectTagNames.has(tag));
|
|
50
|
+
if (!hasAccess) {
|
|
51
|
+
throw new Error("Access denied: Your tags do not grant access to this project");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
56
|
+
async function verifyPersonalAccess(projectId, userId) {
|
|
57
|
+
const project = await prisma.project.findUnique({
|
|
58
|
+
where: { id: projectId },
|
|
59
|
+
include: {
|
|
60
|
+
collaborators: {
|
|
61
|
+
where: { userId },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!project) {
|
|
66
|
+
throw new Error("Project not found");
|
|
67
|
+
}
|
|
68
|
+
const isOwner = project.ownerId === userId;
|
|
69
|
+
const isCollaborator = project.collaborators.length > 0;
|
|
70
|
+
if (!isOwner && !isCollaborator) {
|
|
71
|
+
throw new Error("Access denied: You do not have permission to access this project");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
75
|
+
function getGitHubClient(_organizationId) {
|
|
76
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
77
|
+
if (!githubToken) {
|
|
78
|
+
throw new Error("GitHub token not configured");
|
|
79
|
+
}
|
|
80
|
+
return new GitHubProvider(githubToken);
|
|
81
|
+
}
|
|
82
|
+
function slugify(text, maxLength = 50) {
|
|
83
|
+
return text
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
86
|
+
.substring(0, maxLength);
|
|
87
|
+
}
|
|
88
|
+
export async function createMCPServer() {
|
|
89
|
+
const server = new McpServer({
|
|
90
|
+
name: "mtaap",
|
|
91
|
+
version: VERSION,
|
|
92
|
+
});
|
|
93
|
+
const apiKey = process.env.MTAAP_API_KEY;
|
|
94
|
+
if (!apiKey) {
|
|
95
|
+
throw new Error("MTAAP_API_KEY environment variable is required");
|
|
96
|
+
}
|
|
97
|
+
const auth = await getAuthenticatedUser(apiKey);
|
|
98
|
+
server.registerTool("list_projects", {
|
|
99
|
+
description: "List accessible projects (personal + team via tags)",
|
|
100
|
+
}, async (args) => {
|
|
101
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "list_projects");
|
|
102
|
+
const validated = ListProjectsInputSchema.parse(args);
|
|
103
|
+
const projects = await listProjects(auth.userId, auth.organizationId, validated.workspaceType);
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: JSON.stringify(projects, null, 2),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
server.registerTool("list_tasks", {
|
|
114
|
+
description: "Returns available tasks (filterable by project, state)",
|
|
115
|
+
}, async (args) => {
|
|
116
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "list_tasks");
|
|
117
|
+
const validated = ListTasksInputSchema.parse(args);
|
|
118
|
+
if (validated.projectId) {
|
|
119
|
+
await ensureAccessControl(validated.projectId, auth.userId, auth.organizationId);
|
|
120
|
+
}
|
|
121
|
+
const tasks = await listTasks(auth.userId, auth.organizationId, validated.projectId, validated.state, validated.assigneeId);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: JSON.stringify(tasks, null, 2),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
server.registerTool("get_task", {
|
|
132
|
+
description: "Full task details including acceptance criteria",
|
|
133
|
+
}, async (args) => {
|
|
134
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "get_task");
|
|
135
|
+
const validated = GetTaskInputSchema.parse(args);
|
|
136
|
+
const task = await getTaskWithChecks(validated.taskId, auth.userId, auth.organizationId);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify(task, null, 2),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
server.registerTool("assign_task", {
|
|
147
|
+
description: "Atomic claim → creates branch. Fails if already taken.",
|
|
148
|
+
}, async (args) => {
|
|
149
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "assign_task");
|
|
150
|
+
const validated = AssignTaskInputSchema.parse(args);
|
|
151
|
+
const result = await assignTask(validated.projectId, validated.taskId, auth.userId, auth.organizationId, validated.expectedState);
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify(result, null, 2),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("update_progress", {
|
|
162
|
+
description: "Reports status, updates checkboxes, writes checkpoint",
|
|
163
|
+
}, async (args) => {
|
|
164
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "update_progress");
|
|
165
|
+
const validated = UpdateProgressInputSchema.parse(args);
|
|
166
|
+
const result = await updateProgress(validated.taskId, auth.userId, validated.statusMessage, validated.completedCheckpointIds, validated.currentCheckpointIndex);
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: JSON.stringify(result, null, 2),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
server.registerTool("complete_task", {
|
|
177
|
+
description: "Marks complete, triggers PR, deletes local state file",
|
|
178
|
+
}, async (args) => {
|
|
179
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "complete_task");
|
|
180
|
+
const validated = CompleteTaskInputSchema.parse(args);
|
|
181
|
+
const result = await completeTask(validated.projectId, validated.taskId, auth.userId, auth.organizationId, validated.pullRequestTitle, validated.pullRequestBody);
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: JSON.stringify(result, null, 2),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
server.registerTool("check_active_task", {
|
|
192
|
+
description: "Check for resumable task in .mtaap/active-task.json",
|
|
193
|
+
}, async () => {
|
|
194
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "check_active_task");
|
|
195
|
+
const result = await checkActiveTask();
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: JSON.stringify(result, null, 2),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
server.registerTool("report_error", {
|
|
206
|
+
description: "Report unrecoverable error, displays on task in webapp",
|
|
207
|
+
}, async (args) => {
|
|
208
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "report_error");
|
|
209
|
+
const validated = ReportErrorInputSchema.parse(args);
|
|
210
|
+
const result = await reportTaskError(validated.taskId, auth.userId, validated.errorType, validated.errorMessage, validated.context);
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: JSON.stringify(result, null, 2),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
server.registerTool("get_project_context", {
|
|
221
|
+
description: "Returns assembled context (README, stack, conventions)",
|
|
222
|
+
}, async (args) => {
|
|
223
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "get_project_context");
|
|
224
|
+
const validated = GetProjectContextInputSchema.parse(args);
|
|
225
|
+
await ensureAccessControl(validated.projectId, auth.userId, auth.organizationId);
|
|
226
|
+
const context = await getProjectContext(validated.projectId);
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: JSON.stringify(context, null, 2),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
server.registerTool("add_note", {
|
|
237
|
+
description: "Append implementation notes to task",
|
|
238
|
+
}, async (args) => {
|
|
239
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "add_note");
|
|
240
|
+
const validated = AddNoteInputSchema.parse(args);
|
|
241
|
+
const result = await addTaskNote(validated.taskId, auth.userId, validated.content);
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: JSON.stringify(result, null, 2),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
server.registerTool("abandon_task", {
|
|
252
|
+
description: "Unassign from a task and optionally delete the branch",
|
|
253
|
+
}, async (args) => {
|
|
254
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "abandon_task");
|
|
255
|
+
const validated = AbandonTaskInputSchema.parse(args);
|
|
256
|
+
// Validate project access
|
|
257
|
+
await ensureAccessControl(validated.projectId, auth.userId, auth.organizationId || null);
|
|
258
|
+
const result = await abandonTask(validated.taskId, validated.projectId, auth.userId, validated.deleteBranch);
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: JSON.stringify(result, null, 2),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
server.registerTool("create_personal_project", {
|
|
269
|
+
description: "Create project in user's personal workspace",
|
|
270
|
+
}, async (args) => {
|
|
271
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.WRITE, "create_personal_project");
|
|
272
|
+
const validated = CreatePersonalProjectInputSchema.parse(args);
|
|
273
|
+
const result = await createPersonalProject(auth.userId, validated.name, validated.description, validated.repositoryUrl);
|
|
274
|
+
return {
|
|
275
|
+
content: [
|
|
276
|
+
{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: JSON.stringify(result, null, 2),
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
server.registerTool("get_version", {
|
|
284
|
+
description: "Get the current MTAAP version",
|
|
285
|
+
}, async () => {
|
|
286
|
+
assertApiKeyPermission(auth.apiKey, ApiKeyPermission.READ, "get_version");
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: JSON.stringify({
|
|
292
|
+
version: VERSION,
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
}, null, 2),
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
const transport = new StdioServerTransport();
|
|
300
|
+
await server.connect(transport);
|
|
301
|
+
}
|
|
302
|
+
async function listProjects(userId, organizationId, workspaceType = "ALL") {
|
|
303
|
+
const where = {};
|
|
304
|
+
const include = {};
|
|
305
|
+
if (workspaceType === "PERSONAL" || workspaceType === "ALL") {
|
|
306
|
+
where.ownerId = userId;
|
|
307
|
+
include.owner = true;
|
|
308
|
+
}
|
|
309
|
+
if (organizationId && (workspaceType === "TEAM" || workspaceType === "ALL")) {
|
|
310
|
+
where.OR = [
|
|
311
|
+
{ organizationId },
|
|
312
|
+
{ type: ProjectType.PERSONAL, ownerId: userId },
|
|
313
|
+
];
|
|
314
|
+
include.organization = true;
|
|
315
|
+
include.projectTags = {
|
|
316
|
+
include: { tag: true },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const projects = await prisma.project.findMany({
|
|
320
|
+
where,
|
|
321
|
+
include,
|
|
322
|
+
orderBy: { updatedAt: "desc" },
|
|
323
|
+
});
|
|
324
|
+
const formattedProjects = projects.map((project) => ({
|
|
325
|
+
id: project.id,
|
|
326
|
+
name: project.name,
|
|
327
|
+
description: project.description,
|
|
328
|
+
type: project.type,
|
|
329
|
+
origin: project.origin,
|
|
330
|
+
repositoryUrl: project.repositoryUrl,
|
|
331
|
+
baseBranch: project.baseBranch,
|
|
332
|
+
tags: project.projectTags?.map((pt) => pt.tag.name) || [],
|
|
333
|
+
createdAt: project.createdAt,
|
|
334
|
+
updatedAt: project.updatedAt,
|
|
335
|
+
}));
|
|
336
|
+
return formattedProjects;
|
|
337
|
+
}
|
|
338
|
+
async function listTasks(userId, organizationId, projectId, state, assigneeId) {
|
|
339
|
+
const where = { projectId };
|
|
340
|
+
if (organizationId) {
|
|
341
|
+
where.project = {
|
|
342
|
+
organizationId,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (state) {
|
|
346
|
+
where.state = state;
|
|
347
|
+
}
|
|
348
|
+
if (assigneeId) {
|
|
349
|
+
where.assigneeId = assigneeId;
|
|
350
|
+
}
|
|
351
|
+
const tasks = await prisma.task.findMany({
|
|
352
|
+
where,
|
|
353
|
+
include: {
|
|
354
|
+
project: true,
|
|
355
|
+
assignee: true,
|
|
356
|
+
creator: true,
|
|
357
|
+
epic: true,
|
|
358
|
+
},
|
|
359
|
+
orderBy: { createdAt: "desc" },
|
|
360
|
+
});
|
|
361
|
+
const formattedTasks = await Promise.all(tasks.map(async (task) => {
|
|
362
|
+
const acceptanceCriteria = await prisma.acceptanceCriterion.findMany({
|
|
363
|
+
where: { taskId: task.id },
|
|
364
|
+
orderBy: { order: "asc" },
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
id: task.id,
|
|
368
|
+
projectId: task.projectId,
|
|
369
|
+
projectName: task.project?.name || "Unknown",
|
|
370
|
+
epicId: task.epicId,
|
|
371
|
+
epicName: task.epic?.name || null,
|
|
372
|
+
title: task.title,
|
|
373
|
+
description: task.description,
|
|
374
|
+
state: task.state,
|
|
375
|
+
priority: task.priority,
|
|
376
|
+
assigneeId: task.assigneeId,
|
|
377
|
+
assigneeName: task.assignee?.name || null,
|
|
378
|
+
assigneeEmail: task.assignee?.email || null,
|
|
379
|
+
createdBy: task.createdBy,
|
|
380
|
+
createdByName: task.creator?.name || null,
|
|
381
|
+
assignedAt: task.assignedAt,
|
|
382
|
+
startedAt: task.startedAt,
|
|
383
|
+
completedAt: task.completedAt,
|
|
384
|
+
branchName: task.branchName,
|
|
385
|
+
pullRequestUrl: task.pullRequestUrl,
|
|
386
|
+
pullRequestNumber: task.pullRequestNumber,
|
|
387
|
+
errorType: task.errorType,
|
|
388
|
+
errorMessage: task.errorMessage,
|
|
389
|
+
acceptanceCriteria: acceptanceCriteria.map((ac) => ({
|
|
390
|
+
id: ac.id,
|
|
391
|
+
description: ac.description,
|
|
392
|
+
completed: ac.completed,
|
|
393
|
+
completedAt: ac.completedAt,
|
|
394
|
+
order: ac.order,
|
|
395
|
+
})),
|
|
396
|
+
createdAt: task.createdAt,
|
|
397
|
+
updatedAt: task.updatedAt,
|
|
398
|
+
};
|
|
399
|
+
}));
|
|
400
|
+
return formattedTasks;
|
|
401
|
+
}
|
|
402
|
+
async function getTaskWithChecks(taskId, userId, organizationId) {
|
|
403
|
+
const task = await prisma.task.findUnique({
|
|
404
|
+
where: { id: taskId },
|
|
405
|
+
include: {
|
|
406
|
+
project: {
|
|
407
|
+
include: {
|
|
408
|
+
organization: true,
|
|
409
|
+
projectTags: {
|
|
410
|
+
include: { tag: true },
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
epic: true,
|
|
415
|
+
assignee: true,
|
|
416
|
+
creator: true,
|
|
417
|
+
acceptanceCriteria: {
|
|
418
|
+
orderBy: { order: "asc" },
|
|
419
|
+
},
|
|
420
|
+
progressUpdates: {
|
|
421
|
+
orderBy: { createdAt: "desc" },
|
|
422
|
+
take: 10,
|
|
423
|
+
},
|
|
424
|
+
notes: {
|
|
425
|
+
orderBy: { createdAt: "desc" },
|
|
426
|
+
take: 10,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (!task) {
|
|
431
|
+
throw new Error("Task not found");
|
|
432
|
+
}
|
|
433
|
+
if (organizationId && task.project?.organizationId !== organizationId) {
|
|
434
|
+
const userTags = await prisma.userTag.findMany({
|
|
435
|
+
where: { userId },
|
|
436
|
+
include: { tag: true },
|
|
437
|
+
});
|
|
438
|
+
const projectTagNames = new Set(task.project.projectTags?.map((pt) => pt.tag.name) || []);
|
|
439
|
+
const userTagNames = new Set(userTags.map((ut) => ut.tag.name));
|
|
440
|
+
const hasAccess = [...userTagNames].some((tag) => projectTagNames.has(tag));
|
|
441
|
+
if (!hasAccess) {
|
|
442
|
+
throw new Error("Access denied: Your tags do not grant access to this project");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const formattedTask = {
|
|
446
|
+
id: task.id,
|
|
447
|
+
projectId: task.projectId,
|
|
448
|
+
projectName: task.project?.name || "Unknown",
|
|
449
|
+
epicId: task.epicId,
|
|
450
|
+
epicName: task.epic?.name || null,
|
|
451
|
+
title: task.title,
|
|
452
|
+
description: task.description,
|
|
453
|
+
state: task.state,
|
|
454
|
+
priority: task.priority,
|
|
455
|
+
assigneeId: task.assigneeId,
|
|
456
|
+
assigneeName: task.assignee?.name || null,
|
|
457
|
+
assigneeEmail: task.assignee?.email || null,
|
|
458
|
+
createdBy: task.createdBy,
|
|
459
|
+
createdByName: task.creator?.name || null,
|
|
460
|
+
assignedAt: task.assignedAt,
|
|
461
|
+
startedAt: task.startedAt,
|
|
462
|
+
completedAt: task.completedAt,
|
|
463
|
+
branchName: task.branchName,
|
|
464
|
+
pullRequestUrl: task.pullRequestUrl,
|
|
465
|
+
pullRequestNumber: task.pullRequestNumber,
|
|
466
|
+
errorType: task.errorType,
|
|
467
|
+
errorMessage: task.errorMessage,
|
|
468
|
+
acceptanceCriteria: task.acceptanceCriteria.map((ac) => ({
|
|
469
|
+
id: ac.id,
|
|
470
|
+
description: ac.description,
|
|
471
|
+
completed: ac.completed,
|
|
472
|
+
completedAt: ac.completedAt,
|
|
473
|
+
order: ac.order,
|
|
474
|
+
})),
|
|
475
|
+
progressUpdates: task.progressUpdates.map((pu) => ({
|
|
476
|
+
id: pu.id,
|
|
477
|
+
message: pu.message,
|
|
478
|
+
checkpoints: pu.checkpoints,
|
|
479
|
+
userId: pu.userId,
|
|
480
|
+
createdAt: pu.createdAt,
|
|
481
|
+
})),
|
|
482
|
+
notes: task.notes.map((note) => ({
|
|
483
|
+
id: note.id,
|
|
484
|
+
content: note.content,
|
|
485
|
+
userId: note.userId,
|
|
486
|
+
createdAt: note.createdAt,
|
|
487
|
+
})),
|
|
488
|
+
createdAt: task.createdAt,
|
|
489
|
+
updatedAt: task.updatedAt,
|
|
490
|
+
};
|
|
491
|
+
return formattedTask;
|
|
492
|
+
}
|
|
493
|
+
async function assignTask(projectId, taskId, userId, organizationId, expectedState = TaskState.READY) {
|
|
494
|
+
await ensureAccessControl(projectId, userId, organizationId);
|
|
495
|
+
const task = await prisma.task.findUnique({
|
|
496
|
+
where: { id: taskId },
|
|
497
|
+
include: { project: true },
|
|
498
|
+
});
|
|
499
|
+
if (!task) {
|
|
500
|
+
throw new Error("Task not found");
|
|
501
|
+
}
|
|
502
|
+
if (task.state !== expectedState) {
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
taskId,
|
|
506
|
+
currentState: task.state,
|
|
507
|
+
message: `Task is already in state: ${task.state}`,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const repoInfo = GitHubProvider.parseRepositoryUrl(task.project?.repositoryUrl || "");
|
|
511
|
+
if (!repoInfo) {
|
|
512
|
+
throw new Error("Invalid repository URL");
|
|
513
|
+
}
|
|
514
|
+
const github = getGitHubClient(task.project?.organizationId || "");
|
|
515
|
+
const branchSuffix = slugify(task.title);
|
|
516
|
+
const branchName = `feature/${taskId}-${branchSuffix}`;
|
|
517
|
+
const createResult = await github.createBranch({
|
|
518
|
+
owner: repoInfo.owner,
|
|
519
|
+
repo: repoInfo.repo,
|
|
520
|
+
sourceBranch: task.project?.baseBranch || "develop",
|
|
521
|
+
newBranch: branchName,
|
|
522
|
+
});
|
|
523
|
+
if (!createResult.success) {
|
|
524
|
+
throw new Error(`Failed to create branch: ${createResult.error}`);
|
|
525
|
+
}
|
|
526
|
+
const updatedTask = await prisma.task.update({
|
|
527
|
+
where: { id: taskId },
|
|
528
|
+
data: {
|
|
529
|
+
state: TaskState.IN_PROGRESS,
|
|
530
|
+
assigneeId: userId,
|
|
531
|
+
assignedAt: new Date(),
|
|
532
|
+
branchName,
|
|
533
|
+
startedAt: new Date(),
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
taskId: updatedTask.id,
|
|
539
|
+
branchName,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function updateProgress(taskId, userId, statusMessage, completedCheckpointIds, _currentCheckpointIndex) {
|
|
543
|
+
const task = await prisma.task.findUnique({
|
|
544
|
+
where: { id: taskId },
|
|
545
|
+
});
|
|
546
|
+
if (!task) {
|
|
547
|
+
throw new Error("Task not found");
|
|
548
|
+
}
|
|
549
|
+
if (task.assigneeId !== userId) {
|
|
550
|
+
throw new Error("You are not assigned to this task");
|
|
551
|
+
}
|
|
552
|
+
if (completedCheckpointIds && completedCheckpointIds.length > 0) {
|
|
553
|
+
await prisma.acceptanceCriterion.updateMany({
|
|
554
|
+
where: {
|
|
555
|
+
id: { in: completedCheckpointIds },
|
|
556
|
+
},
|
|
557
|
+
data: {
|
|
558
|
+
completed: true,
|
|
559
|
+
completedAt: new Date(),
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (statusMessage) {
|
|
564
|
+
await prisma.progressUpdate.create({
|
|
565
|
+
data: {
|
|
566
|
+
taskId,
|
|
567
|
+
userId,
|
|
568
|
+
message: statusMessage,
|
|
569
|
+
checkpoints: [],
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const updatedTask = await prisma.task.update({
|
|
574
|
+
where: { id: taskId },
|
|
575
|
+
data: {
|
|
576
|
+
updatedAt: new Date(),
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
taskId: updatedTask.id,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async function completeTask(projectId, taskId, userId, organizationId, pullRequestTitle, pullRequestBody) {
|
|
585
|
+
await ensureAccessControl(projectId, userId, organizationId);
|
|
586
|
+
const task = await prisma.task.findUnique({
|
|
587
|
+
where: { id: taskId },
|
|
588
|
+
include: { project: true },
|
|
589
|
+
});
|
|
590
|
+
if (!task) {
|
|
591
|
+
throw new Error("Task not found");
|
|
592
|
+
}
|
|
593
|
+
if (task.assigneeId !== userId) {
|
|
594
|
+
throw new Error("You are not assigned to this task");
|
|
595
|
+
}
|
|
596
|
+
if (task.state !== TaskState.IN_PROGRESS) {
|
|
597
|
+
throw new Error("Task must be in progress to be completed");
|
|
598
|
+
}
|
|
599
|
+
if (!task.branchName) {
|
|
600
|
+
throw new Error("Task has no associated branch");
|
|
601
|
+
}
|
|
602
|
+
const repoInfo = GitHubProvider.parseRepositoryUrl(task.project?.repositoryUrl || "");
|
|
603
|
+
if (!repoInfo) {
|
|
604
|
+
throw new Error("Invalid repository URL");
|
|
605
|
+
}
|
|
606
|
+
const github = getGitHubClient(task.project?.organizationId || "");
|
|
607
|
+
const acceptanceCriteria = await prisma.acceptanceCriterion.findMany({
|
|
608
|
+
where: { taskId },
|
|
609
|
+
orderBy: { order: "asc" },
|
|
610
|
+
});
|
|
611
|
+
const acceptanceChecklist = acceptanceCriteria
|
|
612
|
+
.map((ac) => `- [${ac.completed ? "x" : " "}] ${ac.description}`)
|
|
613
|
+
.join("\n");
|
|
614
|
+
const prTitle = pullRequestTitle || `[${taskId}] ${task.title}`;
|
|
615
|
+
const prBody = pullRequestBody ||
|
|
616
|
+
`${task.description}\n\n### Acceptance Criteria\n\n${acceptanceChecklist}`;
|
|
617
|
+
const createResult = await github.createPR({
|
|
618
|
+
owner: repoInfo.owner,
|
|
619
|
+
repo: repoInfo.repo,
|
|
620
|
+
title: prTitle,
|
|
621
|
+
body: prBody,
|
|
622
|
+
head: task.branchName,
|
|
623
|
+
base: task.project?.baseBranch || "develop",
|
|
624
|
+
});
|
|
625
|
+
if (!createResult.success) {
|
|
626
|
+
throw new Error(`Failed to create PR: ${createResult.error}`);
|
|
627
|
+
}
|
|
628
|
+
const updatedTask = await prisma.task.update({
|
|
629
|
+
where: { id: taskId },
|
|
630
|
+
data: {
|
|
631
|
+
state: TaskState.REVIEW,
|
|
632
|
+
completedAt: new Date(),
|
|
633
|
+
pullRequestUrl: createResult.prUrl,
|
|
634
|
+
pullRequestNumber: createResult.prNumber,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
return {
|
|
638
|
+
success: true,
|
|
639
|
+
taskId: updatedTask.id,
|
|
640
|
+
prUrl: createResult.prUrl,
|
|
641
|
+
prNumber: createResult.prNumber,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function checkActiveTask() {
|
|
645
|
+
const fs = await import("fs");
|
|
646
|
+
const path = await import("path");
|
|
647
|
+
const activeTaskPath = path.join(process.cwd(), ".mtaap", "active-task.json");
|
|
648
|
+
if (!(await fs.promises.access(activeTaskPath).catch(() => false))) {
|
|
649
|
+
return {
|
|
650
|
+
hasActiveTask: false,
|
|
651
|
+
task: null,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const content = await fs.promises.readFile(activeTaskPath, "utf-8");
|
|
655
|
+
const activeTask = JSON.parse(content);
|
|
656
|
+
return {
|
|
657
|
+
hasActiveTask: true,
|
|
658
|
+
task: activeTask,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async function reportTaskError(taskId, userId, errorType, errorMessage, _context) {
|
|
662
|
+
const task = await prisma.task.findUnique({
|
|
663
|
+
where: { id: taskId },
|
|
664
|
+
});
|
|
665
|
+
if (!task) {
|
|
666
|
+
throw new Error("Task not found");
|
|
667
|
+
}
|
|
668
|
+
if (task.assigneeId !== userId) {
|
|
669
|
+
throw new Error("You are not assigned to this task");
|
|
670
|
+
}
|
|
671
|
+
const updatedTask = await prisma.task.update({
|
|
672
|
+
where: { id: taskId },
|
|
673
|
+
data: {
|
|
674
|
+
errorType,
|
|
675
|
+
errorMessage,
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
success: true,
|
|
680
|
+
taskId: updatedTask.id,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async function getProjectContext(projectId) {
|
|
684
|
+
const project = await prisma.project.findUnique({
|
|
685
|
+
where: { id: projectId },
|
|
686
|
+
include: {
|
|
687
|
+
organization: {
|
|
688
|
+
include: { settings: true },
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
if (!project) {
|
|
693
|
+
throw new Error("Project not found");
|
|
694
|
+
}
|
|
695
|
+
const repoInfo = GitHubProvider.parseRepositoryUrl(project.repositoryUrl);
|
|
696
|
+
let readme = "";
|
|
697
|
+
let stack = [];
|
|
698
|
+
if (repoInfo) {
|
|
699
|
+
const github = getGitHubClient(project.organizationId || "");
|
|
700
|
+
const readmeResult = await github.getRepositoryReadme(repoInfo.owner, repoInfo.repo);
|
|
701
|
+
readme = readmeResult || "No README found";
|
|
702
|
+
const packageJsonResult = await github.getRepositoryPackageJson(repoInfo.owner, repoInfo.repo);
|
|
703
|
+
if (packageJsonResult) {
|
|
704
|
+
const packageJson = JSON.parse(packageJsonResult);
|
|
705
|
+
const deps = {
|
|
706
|
+
...packageJson.dependencies,
|
|
707
|
+
...packageJson.devDependencies,
|
|
708
|
+
};
|
|
709
|
+
stack = Object.keys(deps).slice(0, 20);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const recentCompletedTasks = await prisma.task.findMany({
|
|
713
|
+
where: {
|
|
714
|
+
project: {
|
|
715
|
+
id: projectId,
|
|
716
|
+
},
|
|
717
|
+
state: TaskState.DONE,
|
|
718
|
+
},
|
|
719
|
+
orderBy: { completedAt: "desc" },
|
|
720
|
+
take: 10,
|
|
721
|
+
include: {
|
|
722
|
+
creator: {
|
|
723
|
+
select: { id: true, name: true },
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
const conventions = {
|
|
728
|
+
branchPrefix: "feature/",
|
|
729
|
+
commitFormat: project.organization?.settings?.enforceConventionalCommits
|
|
730
|
+
? "conventional"
|
|
731
|
+
: "none",
|
|
732
|
+
testCommand: "npm test",
|
|
733
|
+
baseBranch: project.baseBranch || "develop",
|
|
734
|
+
notes: project.organization?.settings?.maxPersonalProjectsPerUser?.toString() ||
|
|
735
|
+
"",
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
readme: readme.substring(0, 2000),
|
|
739
|
+
stack,
|
|
740
|
+
recentCompleted: recentCompletedTasks.map((task) => ({
|
|
741
|
+
id: task.id,
|
|
742
|
+
title: task.title,
|
|
743
|
+
completedAt: task.completedAt,
|
|
744
|
+
})),
|
|
745
|
+
conventions,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async function addTaskNote(taskId, userId, content) {
|
|
749
|
+
const task = await prisma.task.findUnique({
|
|
750
|
+
where: { id: taskId },
|
|
751
|
+
});
|
|
752
|
+
if (!task) {
|
|
753
|
+
throw new Error("Task not found");
|
|
754
|
+
}
|
|
755
|
+
if (task.assigneeId !== userId) {
|
|
756
|
+
throw new Error("You are not assigned to this task");
|
|
757
|
+
}
|
|
758
|
+
const note = await prisma.taskNote.create({
|
|
759
|
+
data: {
|
|
760
|
+
taskId,
|
|
761
|
+
userId,
|
|
762
|
+
content,
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
success: true,
|
|
767
|
+
noteId: note.id,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
async function abandonTask(taskId, projectId, userId, deleteBranch) {
|
|
771
|
+
const task = await prisma.task.findUnique({
|
|
772
|
+
where: { id: taskId },
|
|
773
|
+
include: { project: true },
|
|
774
|
+
});
|
|
775
|
+
if (!task) {
|
|
776
|
+
throw new Error("Task not found");
|
|
777
|
+
}
|
|
778
|
+
// Validate task belongs to specified project
|
|
779
|
+
if (task.projectId !== projectId) {
|
|
780
|
+
throw new Error("Task does not belong to the specified project");
|
|
781
|
+
}
|
|
782
|
+
if (task.assigneeId !== userId) {
|
|
783
|
+
throw new Error("You can only abandon tasks you are assigned to");
|
|
784
|
+
}
|
|
785
|
+
// Check if repository URL exists before parsing
|
|
786
|
+
const repositoryUrl = task.project?.repositoryUrl;
|
|
787
|
+
const repoInfo = repositoryUrl
|
|
788
|
+
? GitHubProvider.parseRepositoryUrl(repositoryUrl)
|
|
789
|
+
: undefined;
|
|
790
|
+
if (deleteBranch && repoInfo && task.branchName) {
|
|
791
|
+
const github = getGitHubClient(task.project?.organizationId || "");
|
|
792
|
+
try {
|
|
793
|
+
await github.deleteBranch(repoInfo.owner, repoInfo.repo, task.branchName);
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.error("Failed to delete GitHub branch when abandoning task", {
|
|
797
|
+
taskId,
|
|
798
|
+
projectId: task.projectId,
|
|
799
|
+
organizationId: task.project?.organizationId,
|
|
800
|
+
repositoryOwner: repoInfo.owner,
|
|
801
|
+
repositoryName: repoInfo.repo,
|
|
802
|
+
branchName: task.branchName,
|
|
803
|
+
error,
|
|
804
|
+
});
|
|
805
|
+
throw new Error("Failed to delete branch");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Use TaskState enum constant instead of string literal
|
|
809
|
+
const newState = task.state === TaskState.IN_PROGRESS ? TaskState.READY : task.state;
|
|
810
|
+
const updatedTask = await prisma.task.update({
|
|
811
|
+
where: { id: taskId },
|
|
812
|
+
data: {
|
|
813
|
+
state: newState,
|
|
814
|
+
assigneeId: null,
|
|
815
|
+
assignedAt: null,
|
|
816
|
+
startedAt: null,
|
|
817
|
+
updatedAt: new Date(),
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
return {
|
|
821
|
+
success: true,
|
|
822
|
+
taskId: updatedTask.id,
|
|
823
|
+
state: updatedTask.state,
|
|
824
|
+
branchDeleted: deleteBranch ? !!repoInfo && !!task.branchName : false,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function createPersonalProject(userId, name, description, repositoryUrl) {
|
|
828
|
+
const user = await prisma.user.findUnique({
|
|
829
|
+
where: { id: userId },
|
|
830
|
+
include: { organizations: true },
|
|
831
|
+
});
|
|
832
|
+
if (!user) {
|
|
833
|
+
throw new Error("User not found");
|
|
834
|
+
}
|
|
835
|
+
const repoInfo = GitHubProvider.parseRepositoryUrl(repositoryUrl);
|
|
836
|
+
if (!repoInfo) {
|
|
837
|
+
throw new Error("Invalid repository URL");
|
|
838
|
+
}
|
|
839
|
+
const github = getGitHubClient(user.organizations[0]?.organizationId || "");
|
|
840
|
+
const branchInfo = await github.getBranchInfo({
|
|
841
|
+
owner: repoInfo.owner,
|
|
842
|
+
repo: repoInfo.repo,
|
|
843
|
+
branch: "main",
|
|
844
|
+
});
|
|
845
|
+
if (!branchInfo) {
|
|
846
|
+
throw new Error("Could not get repository branch info");
|
|
847
|
+
}
|
|
848
|
+
const project = await prisma.project.create({
|
|
849
|
+
data: {
|
|
850
|
+
name,
|
|
851
|
+
description,
|
|
852
|
+
type: ProjectType.PERSONAL,
|
|
853
|
+
origin: ProjectOrigin.CREATED,
|
|
854
|
+
ownerId: userId,
|
|
855
|
+
organizationId: null,
|
|
856
|
+
repositoryUrl,
|
|
857
|
+
baseBranch: "main",
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
return {
|
|
861
|
+
success: true,
|
|
862
|
+
projectId: project.id,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ApiKeyPermission } from "@mtaap/core";
|
|
2
|
+
export interface ApiKeyPermissionContext {
|
|
3
|
+
id?: string;
|
|
4
|
+
permissions: "READ" | "WRITE" | "ADMIN";
|
|
5
|
+
}
|
|
6
|
+
export declare function assertApiKeyPermission(apiKey: ApiKeyPermissionContext, required: ApiKeyPermission, toolName: string): void;
|
|
7
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAa/C,MAAM,WAAW,uBAAuB;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;CACzC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,EAAE,MAAM,GACf,IAAI,CAoBN"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission levels ranked by access level.
|
|
3
|
+
* Using string values for compatibility between Prisma's enum and @mtaap/core enum.
|
|
4
|
+
*/
|
|
5
|
+
const PERMISSION_RANK = {
|
|
6
|
+
READ: 1,
|
|
7
|
+
WRITE: 2,
|
|
8
|
+
ADMIN: 3,
|
|
9
|
+
};
|
|
10
|
+
export function assertApiKeyPermission(apiKey, required, toolName) {
|
|
11
|
+
const actualRank = PERMISSION_RANK[apiKey.permissions] ?? 0;
|
|
12
|
+
const requiredRank = PERMISSION_RANK[required] ?? 0;
|
|
13
|
+
if (actualRank >= requiredRank) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.warn("API key permission violation", {
|
|
17
|
+
keyId: apiKey.id,
|
|
18
|
+
requiredPermission: required,
|
|
19
|
+
actualPermission: apiKey.permissions,
|
|
20
|
+
tool: toolName,
|
|
21
|
+
});
|
|
22
|
+
const error = new Error(`API key lacks required permissions (required: ${required})`);
|
|
23
|
+
error.status = 403;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mtaap/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mtaap-mcp": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"start": "node dist/cli.js",
|
|
25
|
+
"lint": "eslint src --ext .ts",
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:ui": "vitest --ui"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
33
|
+
"zod": "^4.3.5",
|
|
34
|
+
"@mtaap/core": "workspace:*",
|
|
35
|
+
"@mtaap/db": "workspace:*",
|
|
36
|
+
"@mtaap/git": "workspace:*",
|
|
37
|
+
"@mtaap/auth": "workspace:*"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@mtaap/config-typescript": "workspace:*",
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"eslint": "^9.39.2",
|
|
43
|
+
"@types/node": "^25.0.9",
|
|
44
|
+
"vitest": "^4.0.17",
|
|
45
|
+
"@vitest/ui": "^4.0.17"
|
|
46
|
+
}
|
|
47
|
+
}
|