feedback-layer-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 +102 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +108 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +14 -0
- package/dist/client/api.d.ts +6 -0
- package/dist/client/api.js +24 -0
- package/dist/client/auth.d.ts +20 -0
- package/dist/client/auth.js +37 -0
- package/dist/config/claude-code.d.ts +1 -0
- package/dist/config/claude-code.js +15 -0
- package/dist/config/codex.d.ts +1 -0
- package/dist/config/codex.js +26 -0
- package/dist/heartbeat.d.ts +3 -0
- package/dist/heartbeat.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +109 -0
- package/dist/tools/claim-task.d.ts +14 -0
- package/dist/tools/claim-task.js +18 -0
- package/dist/tools/complete-task.d.ts +17 -0
- package/dist/tools/complete-task.js +19 -0
- package/dist/tools/get-task.d.ts +14 -0
- package/dist/tools/get-task.js +15 -0
- package/dist/tools/list-tasks.d.ts +19 -0
- package/dist/tools/list-tasks.js +22 -0
- package/dist/tools/release-task.d.ts +14 -0
- package/dist/tools/release-task.js +18 -0
- package/package.json +56 -0
- package/server.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Feedback Layer MCP
|
|
2
|
+
|
|
3
|
+
Feedback Layer MCP exposes selected project feedback requests to coding agents.
|
|
4
|
+
It runs as a local stdio MCP server and talks to your Feedback Layer API over
|
|
5
|
+
HTTPS with a personal access token.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y feedback-layer-mcp@latest init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The init command stores credentials in:
|
|
14
|
+
|
|
15
|
+
```txt
|
|
16
|
+
~/.config/feedback-layer/auth.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It also writes local MCP client configuration for Claude-style `.mcp.json` and
|
|
20
|
+
Codex `~/.codex/config.toml`.
|
|
21
|
+
|
|
22
|
+
## Standard MCP Config
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"feedback-layer": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "feedback-layer-mcp@latest"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Codex
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
codex mcp add feedback-layer npx "feedback-layer-mcp@latest"
|
|
39
|
+
npx -y feedback-layer-mcp@latest init
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or edit `~/.codex/config.toml`:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[mcp_servers.feedback-layer]
|
|
46
|
+
command = "npx"
|
|
47
|
+
args = ["-y", "feedback-layer-mcp@latest"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Claude Code
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add feedback-layer npx feedback-layer-mcp@latest
|
|
54
|
+
npx -y feedback-layer-mcp@latest init
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Cursor, Windsurf, VS Code and Other Clients
|
|
58
|
+
|
|
59
|
+
Use the standard MCP config above. Feedback Layer uses stdio transport, so the
|
|
60
|
+
client starts the package locally with `npx`.
|
|
61
|
+
|
|
62
|
+
## Non-Interactive Setup
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx -y feedback-layer-mcp@latest init \
|
|
66
|
+
--url https://feedback-layer.venture-ia.com \
|
|
67
|
+
--token flp_live_xxx \
|
|
68
|
+
--project findy
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Environment-only configuration also works:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
FL_API_BASE_URL=https://feedback-layer.venture-ia.com \
|
|
75
|
+
FL_PROJECT_SLUG=findy \
|
|
76
|
+
FL_TOKEN=flp_live_xxx \
|
|
77
|
+
npx -y feedback-layer-mcp@latest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
feedback-layer-mcp init
|
|
84
|
+
feedback-layer-mcp doctor
|
|
85
|
+
feedback-layer-mcp status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The short alias `fl-mcp` is also available.
|
|
89
|
+
|
|
90
|
+
## Tools
|
|
91
|
+
|
|
92
|
+
- `list_tasks` - list ready tasks for the configured project
|
|
93
|
+
- `get_task` - read one task with implementation prompt and acceptance criteria
|
|
94
|
+
- `claim_task` - claim a task for the current local session
|
|
95
|
+
- `complete_task` - mark a claimed task complete with a PR URL
|
|
96
|
+
- `release_task` - release a claimed task back to the ready queue
|
|
97
|
+
|
|
98
|
+
## Security
|
|
99
|
+
|
|
100
|
+
Do not commit tokens or generated client config. Keep `.mcp.json` local when it
|
|
101
|
+
contains environment secrets. Prefer `init`, which stores the token in a
|
|
102
|
+
user-local file with `0600` permissions.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type InitFlags = {
|
|
2
|
+
apiBaseUrl?: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
projectSlug?: string;
|
|
5
|
+
repoUrl?: string;
|
|
6
|
+
help?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseInitFlags(args: string[]): InitFlags;
|
|
9
|
+
export declare function runInit(args?: string[]): Promise<void>;
|
|
10
|
+
export {};
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
5
|
+
import { DEFAULT_API_BASE_URL, readAuthConfig, writeAuthConfig, } from "../client/auth.js";
|
|
6
|
+
import { writeClaudeConfig } from "../config/claude-code.js";
|
|
7
|
+
import { writeCodexConfig } from "../config/codex.js";
|
|
8
|
+
function readFlagValue(args, index, flag) {
|
|
9
|
+
const value = args[index + 1];
|
|
10
|
+
if (!value || value.startsWith("--")) {
|
|
11
|
+
throw new Error(`Missing value for ${flag}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function parseInitFlags(args) {
|
|
16
|
+
const flags = {};
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg === "--help" || arg === "-h") {
|
|
20
|
+
flags.help = true;
|
|
21
|
+
}
|
|
22
|
+
else if (arg === "--url" || arg === "--api-base-url") {
|
|
23
|
+
flags.apiBaseUrl = readFlagValue(args, i, arg);
|
|
24
|
+
i += 1;
|
|
25
|
+
}
|
|
26
|
+
else if (arg === "--token") {
|
|
27
|
+
flags.token = readFlagValue(args, i, arg);
|
|
28
|
+
i += 1;
|
|
29
|
+
}
|
|
30
|
+
else if (arg === "--project" || arg === "--project-slug") {
|
|
31
|
+
flags.projectSlug = readFlagValue(args, i, arg);
|
|
32
|
+
i += 1;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === "--repo-url") {
|
|
35
|
+
flags.repoUrl = readFlagValue(args, i, arg);
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`Unknown init option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return flags;
|
|
43
|
+
}
|
|
44
|
+
function printInitHelp() {
|
|
45
|
+
console.log(`Usage: feedback-layer-mcp init [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--url <url> Feedback Layer URL. Defaults to ${DEFAULT_API_BASE_URL}
|
|
49
|
+
--token <token> Feedback Layer MCP token
|
|
50
|
+
--project <slug> Project slug, for example findy
|
|
51
|
+
--repo-url <url> Repository URL used for project auto-detection
|
|
52
|
+
-h, --help Show this help
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
async function detectRepoRemote() {
|
|
56
|
+
try {
|
|
57
|
+
const gitConfig = await readFile(".git/config", "utf8");
|
|
58
|
+
const match = gitConfig.match(/\[remote "origin"\][\s\S]*?url = (.+)/);
|
|
59
|
+
return match?.[1]?.trim() ?? null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function runInit(args = []) {
|
|
66
|
+
const flags = parseInitFlags(args);
|
|
67
|
+
if (flags.help) {
|
|
68
|
+
printInitHelp();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const rl = createInterface({ input, output });
|
|
72
|
+
const existing = await readAuthConfig();
|
|
73
|
+
const apiBaseUrl = flags.apiBaseUrl ||
|
|
74
|
+
(await rl.question(`Feedback Layer URL (${existing.apiBaseUrl ?? DEFAULT_API_BASE_URL}): `)) ||
|
|
75
|
+
existing.apiBaseUrl ||
|
|
76
|
+
DEFAULT_API_BASE_URL;
|
|
77
|
+
const token = flags.token ||
|
|
78
|
+
(await rl.question("Paste Feedback Layer MCP token: ")) ||
|
|
79
|
+
existing.token;
|
|
80
|
+
if (!token)
|
|
81
|
+
throw new Error("Missing Feedback Layer MCP token");
|
|
82
|
+
await writeAuthConfig({ ...existing, apiBaseUrl, token });
|
|
83
|
+
let project = flags.projectSlug
|
|
84
|
+
? { slug: flags.projectSlug, name: flags.projectSlug }
|
|
85
|
+
: null;
|
|
86
|
+
if (!project) {
|
|
87
|
+
const repoUrl = flags.repoUrl || (await detectRepoRemote());
|
|
88
|
+
const qs = repoUrl ? `?repoUrl=${encodeURIComponent(repoUrl)}` : "";
|
|
89
|
+
const detected = await feedbackLayerApi(`/api/mcp/projects${qs}`);
|
|
90
|
+
project =
|
|
91
|
+
detected.projects[0] ??
|
|
92
|
+
(() => {
|
|
93
|
+
throw new Error("No MCP-enabled project matched this token/repo");
|
|
94
|
+
})();
|
|
95
|
+
}
|
|
96
|
+
await writeAuthConfig({
|
|
97
|
+
...existing,
|
|
98
|
+
apiBaseUrl,
|
|
99
|
+
token,
|
|
100
|
+
projectSlug: project.slug,
|
|
101
|
+
});
|
|
102
|
+
await writeClaudeConfig(project.slug);
|
|
103
|
+
await writeCodexConfig(project.slug);
|
|
104
|
+
rl.close();
|
|
105
|
+
console.log(`Detected project: ${project.name} (${project.slug})`);
|
|
106
|
+
console.log("Wrote .mcp.json");
|
|
107
|
+
console.log("Updated ~/.codex/config.toml");
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
import { resolveProjectRuntimeConfig } from "../client/auth.js";
|
|
3
|
+
export async function runStatus() {
|
|
4
|
+
const config = await resolveProjectRuntimeConfig();
|
|
5
|
+
const result = await feedbackLayerApi(`/api/mcp/tasks?project=${encodeURIComponent(config.projectSlug)}&limit=5`);
|
|
6
|
+
console.log(JSON.stringify({ config: { ...config, token: "[redacted]" }, result }, null, 2));
|
|
7
|
+
}
|
|
8
|
+
export async function runDoctor() {
|
|
9
|
+
const config = await resolveProjectRuntimeConfig();
|
|
10
|
+
await feedbackLayerApi(`/api/mcp/tasks?project=${encodeURIComponent(config.projectSlug)}&limit=1`);
|
|
11
|
+
console.log("Feedback Layer MCP doctor: ok");
|
|
12
|
+
console.log(`API: ${config.apiBaseUrl}`);
|
|
13
|
+
console.log(`Project: ${config.projectSlug}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveRuntimeConfig } from "./auth.js";
|
|
2
|
+
export async function feedbackLayerApi(path, options = {}) {
|
|
3
|
+
const config = await resolveRuntimeConfig();
|
|
4
|
+
const url = new URL(path, config.apiBaseUrl);
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
method: options.method ?? "GET",
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${config.token}`,
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
12
|
+
});
|
|
13
|
+
const text = await res.text();
|
|
14
|
+
const data = text ? JSON.parse(text) : {};
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
const message = typeof data?.message === "string"
|
|
17
|
+
? data.message
|
|
18
|
+
: typeof data?.error === "string"
|
|
19
|
+
? data.error
|
|
20
|
+
: `Feedback Layer API ${res.status}`;
|
|
21
|
+
throw new Error(message);
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type AuthConfig = {
|
|
2
|
+
token?: string;
|
|
3
|
+
apiBaseUrl?: string;
|
|
4
|
+
projectSlug?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const DEFAULT_API_BASE_URL = "https://feedback-layer.venture-ia.com";
|
|
7
|
+
export declare const AUTH_PATH: string;
|
|
8
|
+
export declare function readAuthConfig(): Promise<AuthConfig>;
|
|
9
|
+
export declare function writeAuthConfig(config: AuthConfig): Promise<void>;
|
|
10
|
+
export declare function resolveRuntimeConfig(): Promise<{
|
|
11
|
+
token: string;
|
|
12
|
+
apiBaseUrl: string;
|
|
13
|
+
projectSlug: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function resolveProjectRuntimeConfig(): Promise<{
|
|
16
|
+
projectSlug: string;
|
|
17
|
+
token: string;
|
|
18
|
+
apiBaseUrl: string;
|
|
19
|
+
}>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export const DEFAULT_API_BASE_URL = "https://feedback-layer.venture-ia.com";
|
|
5
|
+
export const AUTH_PATH = join(homedir(), ".config", "feedback-layer", "auth.json");
|
|
6
|
+
export async function readAuthConfig() {
|
|
7
|
+
try {
|
|
8
|
+
const raw = await readFile(AUTH_PATH, "utf8");
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function writeAuthConfig(config) {
|
|
16
|
+
await mkdir(dirname(AUTH_PATH), { recursive: true });
|
|
17
|
+
await writeFile(AUTH_PATH, `${JSON.stringify(config, null, 2)}\n`, {
|
|
18
|
+
mode: 0o600,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function resolveRuntimeConfig() {
|
|
22
|
+
const file = await readAuthConfig();
|
|
23
|
+
const token = process.env.FL_TOKEN ?? file.token;
|
|
24
|
+
const apiBaseUrl = process.env.FL_API_BASE_URL ?? file.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
25
|
+
const projectSlug = process.env.FL_PROJECT_SLUG ?? file.projectSlug;
|
|
26
|
+
if (!token) {
|
|
27
|
+
throw new Error("Missing Feedback Layer token. Run `feedback-layer-mcp init`.");
|
|
28
|
+
}
|
|
29
|
+
return { token, apiBaseUrl, projectSlug };
|
|
30
|
+
}
|
|
31
|
+
export async function resolveProjectRuntimeConfig() {
|
|
32
|
+
const config = await resolveRuntimeConfig();
|
|
33
|
+
if (!config.projectSlug) {
|
|
34
|
+
throw new Error("Missing project slug. Set FL_PROJECT_SLUG or run init.");
|
|
35
|
+
}
|
|
36
|
+
return { ...config, projectSlug: config.projectSlug };
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeClaudeConfig(projectSlug: string): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
export async function writeClaudeConfig(projectSlug) {
|
|
3
|
+
const config = {
|
|
4
|
+
mcpServers: {
|
|
5
|
+
"feedback-layer": {
|
|
6
|
+
command: "npx",
|
|
7
|
+
args: ["-y", "feedback-layer-mcp@latest"],
|
|
8
|
+
env: {
|
|
9
|
+
FL_PROJECT_SLUG: projectSlug,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
await writeFile(".mcp.json", `${JSON.stringify(config, null, 2)}\n`);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeCodexConfig(projectSlug: string): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
|
|
5
|
+
export async function writeCodexConfig(projectSlug) {
|
|
6
|
+
let current = "";
|
|
7
|
+
try {
|
|
8
|
+
current = await readFile(CODEX_CONFIG, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
current = "";
|
|
12
|
+
}
|
|
13
|
+
const block = `[mcp_servers.feedback-layer]
|
|
14
|
+
command = "npx"
|
|
15
|
+
args = ["-y", "feedback-layer-mcp@latest"]
|
|
16
|
+
|
|
17
|
+
[mcp_servers.feedback-layer.env]
|
|
18
|
+
FL_PROJECT_SLUG = "${projectSlug}"
|
|
19
|
+
`;
|
|
20
|
+
const withoutExisting = current
|
|
21
|
+
.replace(/\n?\[mcp_servers\.feedback-layer\][\s\S]*?(?=\n\[|\s*$)/g, "")
|
|
22
|
+
.replace(/\n?\[mcp_servers\.feedback-layer\.env\][\s\S]*?(?=\n\[|\s*$)/g, "");
|
|
23
|
+
await mkdir(dirname(CODEX_CONFIG), { recursive: true });
|
|
24
|
+
const nextConfig = `${withoutExisting.trimEnd()}\n\n${block}`.trimStart();
|
|
25
|
+
await writeFile(CODEX_CONFIG, `${nextConfig}\n`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "./client/api.js";
|
|
2
|
+
const activeClaims = new Map();
|
|
3
|
+
export function startHeartbeat(taskId, sessionId) {
|
|
4
|
+
stopHeartbeat(taskId);
|
|
5
|
+
const timer = setInterval(async () => {
|
|
6
|
+
try {
|
|
7
|
+
await feedbackLayerApi(`/api/mcp/tasks/${taskId}/heartbeat`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
body: { sessionId },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
console.error(`[feedback-layer] heartbeat failed for ${taskId}`, err);
|
|
14
|
+
stopHeartbeat(taskId);
|
|
15
|
+
}
|
|
16
|
+
}, 60_000);
|
|
17
|
+
activeClaims.set(taskId, timer);
|
|
18
|
+
}
|
|
19
|
+
export function stopHeartbeat(taskId) {
|
|
20
|
+
const timer = activeClaims.get(taskId);
|
|
21
|
+
if (timer)
|
|
22
|
+
clearInterval(timer);
|
|
23
|
+
activeClaims.delete(taskId);
|
|
24
|
+
}
|
|
25
|
+
export async function releaseActiveClaims(sessionId) {
|
|
26
|
+
for (const taskId of activeClaims.keys()) {
|
|
27
|
+
stopHeartbeat(taskId);
|
|
28
|
+
await feedbackLayerApi(`/api/mcp/tasks/${taskId}/release`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: { sessionId },
|
|
31
|
+
}).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { resolveProjectRuntimeConfig } from "./client/auth.js";
|
|
9
|
+
import { releaseActiveClaims, startHeartbeat, stopHeartbeat } from "./heartbeat.js";
|
|
10
|
+
import { runInit } from "./cli/init.js";
|
|
11
|
+
import { runDoctor, runStatus } from "./cli/status.js";
|
|
12
|
+
import { claimTask, claimTaskTool } from "./tools/claim-task.js";
|
|
13
|
+
import { completeTask, completeTaskTool } from "./tools/complete-task.js";
|
|
14
|
+
import { getTask, getTaskTool } from "./tools/get-task.js";
|
|
15
|
+
import { listTasks, listTasksTool } from "./tools/list-tasks.js";
|
|
16
|
+
import { releaseTask, releaseTaskTool } from "./tools/release-task.js";
|
|
17
|
+
const sessionId = process.env.FL_SESSION_ID ??
|
|
18
|
+
crypto
|
|
19
|
+
.createHash("sha256")
|
|
20
|
+
.update(`${process.cwd()}|${os.hostname()}|${process.pid}`)
|
|
21
|
+
.digest("hex")
|
|
22
|
+
.slice(0, 24);
|
|
23
|
+
const tools = [
|
|
24
|
+
listTasksTool,
|
|
25
|
+
getTaskTool,
|
|
26
|
+
claimTaskTool,
|
|
27
|
+
completeTaskTool,
|
|
28
|
+
releaseTaskTool,
|
|
29
|
+
];
|
|
30
|
+
function asRecord(value) {
|
|
31
|
+
return value && typeof value === "object"
|
|
32
|
+
? value
|
|
33
|
+
: {};
|
|
34
|
+
}
|
|
35
|
+
async function callTool(name, args) {
|
|
36
|
+
const config = await resolveProjectRuntimeConfig();
|
|
37
|
+
if (name === "list_tasks")
|
|
38
|
+
return listTasks(args, config.projectSlug);
|
|
39
|
+
if (name === "get_task")
|
|
40
|
+
return getTask(args);
|
|
41
|
+
if (name === "claim_task") {
|
|
42
|
+
const result = await claimTask(args, sessionId);
|
|
43
|
+
if (typeof args.taskId === "string")
|
|
44
|
+
startHeartbeat(args.taskId, sessionId);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
if (name === "complete_task") {
|
|
48
|
+
const result = await completeTask(args, sessionId);
|
|
49
|
+
if (typeof args.taskId === "string")
|
|
50
|
+
stopHeartbeat(args.taskId);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
if (name === "release_task") {
|
|
54
|
+
const result = await releaseTask(args, sessionId);
|
|
55
|
+
if (typeof args.taskId === "string")
|
|
56
|
+
stopHeartbeat(args.taskId);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
60
|
+
}
|
|
61
|
+
async function startServer() {
|
|
62
|
+
const server = new Server({ name: "feedback-layer", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
63
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
64
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
65
|
+
const result = await callTool(request.params.name, asRecord(request.params.arguments));
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: JSON.stringify(result, null, 2),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const transport = new StdioServerTransport();
|
|
76
|
+
await server.connect(transport);
|
|
77
|
+
}
|
|
78
|
+
async function main() {
|
|
79
|
+
const command = process.argv[2];
|
|
80
|
+
if (command === "init")
|
|
81
|
+
return runInit(process.argv.slice(3));
|
|
82
|
+
if (command === "status")
|
|
83
|
+
return runStatus();
|
|
84
|
+
if (command === "doctor")
|
|
85
|
+
return runDoctor();
|
|
86
|
+
if (command === "--help" || command === "-h") {
|
|
87
|
+
console.log(`Usage: feedback-layer-mcp [command]
|
|
88
|
+
|
|
89
|
+
Commands:
|
|
90
|
+
init Configure local auth and MCP client files
|
|
91
|
+
doctor Verify API connectivity for the configured project
|
|
92
|
+
status Print the current configuration and first tasks
|
|
93
|
+
|
|
94
|
+
Run without a command to start the MCP stdio server.
|
|
95
|
+
`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
return startServer();
|
|
99
|
+
}
|
|
100
|
+
process.on("SIGINT", () => {
|
|
101
|
+
releaseActiveClaims(sessionId).finally(() => process.exit(0));
|
|
102
|
+
});
|
|
103
|
+
process.on("SIGTERM", () => {
|
|
104
|
+
releaseActiveClaims(sessionId).finally(() => process.exit(0));
|
|
105
|
+
});
|
|
106
|
+
main().catch((err) => {
|
|
107
|
+
console.error(err instanceof Error ? err.message : err);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const claimTaskTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
taskId: {
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
required: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function claimTask(args: Record<string, unknown>, sessionId: string): Promise<unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
export const claimTaskTool = {
|
|
3
|
+
name: "claim_task",
|
|
4
|
+
description: "Claim a task for this local session. The server locks it for 5 minutes; the MCP server heartbeats while active.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
taskId: { type: "string" },
|
|
9
|
+
},
|
|
10
|
+
required: ["taskId"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export async function claimTask(args, sessionId) {
|
|
14
|
+
return feedbackLayerApi(`/api/mcp/tasks/${String(args.taskId)}/claim`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
body: { sessionId },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const completeTaskTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
taskId: {
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
prUrl: {
|
|
11
|
+
type: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
required: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare function completeTask(args: Record<string, unknown>, sessionId: string): Promise<unknown>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
export const completeTaskTool = {
|
|
3
|
+
name: "complete_task",
|
|
4
|
+
description: "Mark the claimed task complete for review. Requires a PR URL and moves the task to `en_review`.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
taskId: { type: "string" },
|
|
9
|
+
prUrl: { type: "string" },
|
|
10
|
+
},
|
|
11
|
+
required: ["taskId", "prUrl"],
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export async function completeTask(args, sessionId) {
|
|
15
|
+
return feedbackLayerApi(`/api/mcp/tasks/${String(args.taskId)}/complete`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
body: { sessionId, prUrl: String(args.prUrl) },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const getTaskTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
taskId: {
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
required: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function getTask(args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
export const getTaskTool = {
|
|
3
|
+
name: "get_task",
|
|
4
|
+
description: "Get a Feedback Layer task by id, including the generated implementation prompt and acceptance criteria.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
taskId: { type: "string" },
|
|
9
|
+
},
|
|
10
|
+
required: ["taskId"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export async function getTask(args) {
|
|
14
|
+
return feedbackLayerApi(`/api/mcp/tasks/${String(args.taskId)}`);
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const listTasksTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
limit: {
|
|
8
|
+
type: string;
|
|
9
|
+
default: number;
|
|
10
|
+
maximum: number;
|
|
11
|
+
};
|
|
12
|
+
minConfidence: {
|
|
13
|
+
type: string;
|
|
14
|
+
default: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare function listTasks(args: Record<string, unknown>, projectSlug: string): Promise<unknown>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
export const listTasksTool = {
|
|
3
|
+
name: "list_tasks",
|
|
4
|
+
description: "List ready Feedback Layer tasks for the configured project. Returns unclaimed `pret_ia` tasks sorted by priority.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
limit: { type: "integer", default: 10, maximum: 50 },
|
|
9
|
+
minConfidence: { type: "number", default: 0.5 },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export async function listTasks(args, projectSlug) {
|
|
14
|
+
const limit = Number(args.limit ?? 10);
|
|
15
|
+
const minConfidence = Number(args.minConfidence ?? 0.5);
|
|
16
|
+
const qs = new URLSearchParams({
|
|
17
|
+
project: projectSlug,
|
|
18
|
+
limit: String(limit),
|
|
19
|
+
minConfidence: String(minConfidence),
|
|
20
|
+
});
|
|
21
|
+
return feedbackLayerApi(`/api/mcp/tasks?${qs.toString()}`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const releaseTaskTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
taskId: {
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
required: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function releaseTask(args: Record<string, unknown>, sessionId: string): Promise<unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { feedbackLayerApi } from "../client/api.js";
|
|
2
|
+
export const releaseTaskTool = {
|
|
3
|
+
name: "release_task",
|
|
4
|
+
description: "Release the current claim for a task and return it to the ready queue if it is still MCP-owned.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
taskId: { type: "string" },
|
|
9
|
+
},
|
|
10
|
+
required: ["taskId"],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export async function releaseTask(args, sessionId) {
|
|
14
|
+
return feedbackLayerApi(`/api/mcp/tasks/${String(args.taskId)}/release`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
body: { sessionId },
|
|
17
|
+
});
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feedback-layer-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Feedback Layer MCP server for exposing selected feedback requests to coding agents.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/VentureIA/feedback-layer.git",
|
|
8
|
+
"directory": "packages/mcp"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/VentureIA/feedback-layer#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/VentureIA/feedback-layer/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"mcpName": "io.github.ventureia/feedback-layer-mcp",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"feedback-layer-mcp": "dist/index.js",
|
|
28
|
+
"fl-mcp": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"server.json",
|
|
34
|
+
"package.json"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"dev": "tsx src/index.ts",
|
|
45
|
+
"doctor": "node dist/index.js doctor",
|
|
46
|
+
"prepack": "npm run build",
|
|
47
|
+
"start": "node dist/index.js"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"tsx": "^4.19.2",
|
|
54
|
+
"typescript": "^5"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.ventureia/feedback-layer-mcp",
|
|
4
|
+
"description": "Feedback Layer MCP server for exposing selected feedback requests to coding agents.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/VentureIA/feedback-layer",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "feedback-layer-mcp",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|