agent-config-mcp 1.0.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 +41 -0
- package/index.js +150 -0
- package/package.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# agent-config-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for syncing agent config (`.kiro/`) via Git. Changes are submitted as PRs for review.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `ac_init(repo)` | Clone agent config repo into `.kiro/` |
|
|
10
|
+
| `ac_status` | Show sync status (changed files, ahead/behind) |
|
|
11
|
+
| `ac_push(message?)` | Create branch → commit → push → open PR |
|
|
12
|
+
| `ac_pull` | Pull latest from main |
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Add to your MCP config (`mcp.json`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"agent-config": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "agent-config-mcp"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## First use
|
|
28
|
+
|
|
29
|
+
Ask your agent to run `ac_init` with your config repo URL:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
ac_init({ repo: "https://github.com/your-org/your-agent-config.git" })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This clones the repo into `.kiro/` of your current project. After that, `ac_push` and `ac_pull` work automatically.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Node.js 18+
|
|
40
|
+
- Git
|
|
41
|
+
- GitHub CLI (`gh`) — for PR creation
|
package/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
const CONFIG_DIR = ".kiro";
|
|
12
|
+
|
|
13
|
+
function configPath() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
return join(cwd, CONFIG_DIR);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function git(args, cwd) {
|
|
19
|
+
const { stdout, stderr } = await exec("git", args, { cwd });
|
|
20
|
+
return stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isGitRepo(dir) {
|
|
24
|
+
return existsSync(join(dir, ".git"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: "agent-config-mcp",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ac_status
|
|
33
|
+
server.tool(
|
|
34
|
+
"ac_status",
|
|
35
|
+
"Show sync status of .kiro/ config directory",
|
|
36
|
+
{},
|
|
37
|
+
async () => {
|
|
38
|
+
const dir = configPath();
|
|
39
|
+
if (!existsSync(dir)) {
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: ".kiro/ directory not found" }) }] };
|
|
41
|
+
}
|
|
42
|
+
if (!isGitRepo(dir)) {
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: ".kiro/ is not a git repository. Run ac_init first." }) }] };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const status = await git(["status", "--porcelain"], dir);
|
|
47
|
+
const log = await git(["log", "-1", "--format=%h %s (%cr)"], dir);
|
|
48
|
+
const remote = await git(["remote", "get-url", "origin"], dir);
|
|
49
|
+
let ahead = 0, behind = 0;
|
|
50
|
+
try {
|
|
51
|
+
await git(["fetch", "--quiet"], dir);
|
|
52
|
+
const ab = await git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], dir);
|
|
53
|
+
[ahead, behind] = ab.split("\t").map(Number);
|
|
54
|
+
} catch {}
|
|
55
|
+
const files = status ? status.split("\n").map(l => l.trim()) : [];
|
|
56
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
57
|
+
success: true, remote, lastCommit: log,
|
|
58
|
+
changed: files.length, files, ahead, behind
|
|
59
|
+
}) }] };
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ac_pull
|
|
67
|
+
server.tool(
|
|
68
|
+
"ac_pull",
|
|
69
|
+
"Pull latest changes from remote",
|
|
70
|
+
{},
|
|
71
|
+
async () => {
|
|
72
|
+
const dir = configPath();
|
|
73
|
+
if (!isGitRepo(dir)) {
|
|
74
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: ".kiro/ is not a git repository" }) }] };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const result = await git(["pull", "--rebase"], dir);
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: result || "Already up to date" }) }] };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e.message.includes("CONFLICT")) {
|
|
81
|
+
const conflicts = await git(["diff", "--name-only", "--diff-filter=U"], dir);
|
|
82
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "Conflict", conflicts: conflicts.split("\n") }) }] };
|
|
83
|
+
}
|
|
84
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// ac_push
|
|
90
|
+
server.tool(
|
|
91
|
+
"ac_push",
|
|
92
|
+
"Create a branch, commit changes, push, and open a PR",
|
|
93
|
+
{ message: z.string().optional().describe("Commit/PR message (auto-generated if omitted)") },
|
|
94
|
+
async ({ message }) => {
|
|
95
|
+
const dir = configPath();
|
|
96
|
+
if (!isGitRepo(dir)) {
|
|
97
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: ".kiro/ is not a git repository" }) }] };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await git(["add", "-A"], dir);
|
|
101
|
+
const status = await git(["status", "--porcelain"], dir);
|
|
102
|
+
if (!status) {
|
|
103
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "No changes to push" }) }] };
|
|
104
|
+
}
|
|
105
|
+
const files = status.split("\n").map(l => l.trim());
|
|
106
|
+
const msg = message || `sync: ${files.map(f => f.slice(3)).join(", ")}`;
|
|
107
|
+
// Create branch, commit, push
|
|
108
|
+
const branch = `update/${Date.now()}`;
|
|
109
|
+
await git(["checkout", "-b", branch], dir);
|
|
110
|
+
await git(["commit", "-m", msg], dir);
|
|
111
|
+
await git(["push", "-u", "origin", branch], dir);
|
|
112
|
+
// Create PR via gh CLI
|
|
113
|
+
const { stdout: prUrl } = await exec("gh", ["pr", "create", "--title", msg, "--body", files.map(f => `- ${f.slice(3)}`).join("\n"), "--base", "main"], { cwd: dir });
|
|
114
|
+
// Return to main
|
|
115
|
+
await git(["checkout", "main"], dir);
|
|
116
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
117
|
+
success: true, message: "PR created", pr: prUrl.trim(), branch, files: files.map(f => f.slice(3))
|
|
118
|
+
}) }] };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// ac_init
|
|
126
|
+
server.tool(
|
|
127
|
+
"ac_init",
|
|
128
|
+
"Clone agent config repo into .kiro/",
|
|
129
|
+
{ repo: z.string().describe("Git repository URL") },
|
|
130
|
+
async ({ repo }) => {
|
|
131
|
+
const dir = configPath();
|
|
132
|
+
if (isGitRepo(dir)) {
|
|
133
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: ".kiro/ is already a git repository" }) }] };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
// If .kiro/ exists with files, back up first
|
|
137
|
+
if (existsSync(dir)) {
|
|
138
|
+
const backupDir = `${dir}.backup.${Date.now()}`;
|
|
139
|
+
await exec("mv", [dir, backupDir]);
|
|
140
|
+
}
|
|
141
|
+
await exec("git", ["clone", repo, CONFIG_DIR], { cwd: process.cwd() });
|
|
142
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Cloned ${repo} into ${CONFIG_DIR}/` }) }] };
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: e.message }) }] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const transport = new StdioServerTransport();
|
|
150
|
+
await server.connect(transport);
|