@askmesh/mcp 0.2.0 → 0.3.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/dist/agent/auto_responder.d.ts +3 -2
- package/dist/agent/auto_responder.js +40 -50
- package/dist/client/askmesh_client.d.ts +6 -0
- package/dist/client/askmesh_client.js +8 -0
- package/dist/index.js +9 -14
- package/dist/sse/sse_listener.d.ts +5 -1
- package/dist/sse/sse_listener.js +11 -7
- package/dist/tools/ask_agent.js +21 -2
- package/dist/tools/askmesh.d.ts +3 -0
- package/dist/tools/askmesh.js +84 -0
- package/package.json +1 -1
- package/dist/agent/context_reader.d.ts +0 -1
- package/dist/agent/context_reader.js +0 -68
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AskMeshClient } from '../client/askmesh_client.js';
|
|
2
2
|
import type { IncomingRequest } from '../sse/sse_listener.js';
|
|
3
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
4
|
export declare class AutoResponder {
|
|
4
|
-
private anthropic;
|
|
5
5
|
private client;
|
|
6
|
-
private
|
|
6
|
+
private mcpServer;
|
|
7
7
|
constructor(client: AskMeshClient);
|
|
8
|
+
setServer(server: Server): void;
|
|
8
9
|
handleRequest(request: IncomingRequest): Promise<void>;
|
|
9
10
|
}
|
|
@@ -1,61 +1,51 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import { readLocalContext } from './context_reader.js';
|
|
3
|
-
const SYSTEM_PROMPT = `You are an AI coding agent responding on behalf of a developer through AskMesh.
|
|
4
|
-
You have access to the developer's project context, CLAUDE.md, and memories below.
|
|
5
|
-
Use this context to answer the question as if you were the developer's agent.
|
|
6
|
-
|
|
7
|
-
Rules:
|
|
8
|
-
- Answer concisely and technically
|
|
9
|
-
- Reference specific files, conventions, or decisions from the context when relevant
|
|
10
|
-
- If the context doesn't contain enough info to answer, say so honestly
|
|
11
|
-
- Keep responses under 500 words unless the question requires more detail
|
|
12
|
-
- Answer in the same language as the question`;
|
|
13
1
|
export class AutoResponder {
|
|
14
|
-
anthropic;
|
|
15
2
|
client;
|
|
16
|
-
|
|
3
|
+
mcpServer = null;
|
|
17
4
|
constructor(client) {
|
|
18
5
|
this.client = client;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
console.error('[AskMesh] Auto-responder enabled');
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
this.anthropic = null;
|
|
26
|
-
console.error('[AskMesh] Auto-responder disabled (no ANTHROPIC_API_KEY)');
|
|
27
|
-
}
|
|
6
|
+
}
|
|
7
|
+
setServer(server) {
|
|
8
|
+
this.mcpServer = server;
|
|
28
9
|
}
|
|
29
10
|
async handleRequest(request) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
11
|
+
console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
|
|
12
|
+
// Try MCP sampling — asks the active Claude Code to generate a response
|
|
13
|
+
if (this.mcpServer) {
|
|
14
|
+
try {
|
|
15
|
+
const result = (await this.mcpServer.request({
|
|
16
|
+
method: 'sampling/createMessage',
|
|
17
|
+
params: {
|
|
18
|
+
messages: [
|
|
19
|
+
{
|
|
20
|
+
role: 'user',
|
|
21
|
+
content: {
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: [
|
|
24
|
+
`A teammate @${request.fromUsername} is asking you a question via AskMesh.`,
|
|
25
|
+
``,
|
|
26
|
+
`Question: "${request.question}"`,
|
|
27
|
+
request.context ? `\nContext: ${request.context}` : '',
|
|
28
|
+
``,
|
|
29
|
+
`Answer based on your knowledge of this project. Be concise and technical.`,
|
|
30
|
+
`Answer in the same language as the question.`,
|
|
31
|
+
].join('\n'),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
maxTokens: 2048,
|
|
45
36
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
37
|
+
}, {}));
|
|
38
|
+
const answer = result?.content?.text || result?.content?.[0]?.text;
|
|
39
|
+
if (answer) {
|
|
40
|
+
await this.client.answerRequest(request.id, answer);
|
|
41
|
+
console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error('[AskMesh] Sampling not available — question queued');
|
|
55
47
|
}
|
|
56
48
|
}
|
|
57
|
-
|
|
58
|
-
console.error(`[AskMesh] Auto-response failed:`, err);
|
|
59
|
-
}
|
|
49
|
+
console.error(`[AskMesh] Question queued — use answer_pending to respond`);
|
|
60
50
|
}
|
|
61
51
|
}
|
|
@@ -7,6 +7,12 @@ export declare class AskMeshClient {
|
|
|
7
7
|
requestId: number;
|
|
8
8
|
status: string;
|
|
9
9
|
}>;
|
|
10
|
+
getRequest(requestId: number): Promise<{
|
|
11
|
+
id: number;
|
|
12
|
+
status: string;
|
|
13
|
+
answer: string | null;
|
|
14
|
+
question: string;
|
|
15
|
+
}>;
|
|
10
16
|
getPendingRequests(): Promise<{
|
|
11
17
|
requests: Array<{
|
|
12
18
|
id: number;
|
|
@@ -21,6 +21,14 @@ export class AskMeshClient {
|
|
|
21
21
|
throw new Error(`askAgent failed: ${res.status} ${await res.text()}`);
|
|
22
22
|
return res.json();
|
|
23
23
|
}
|
|
24
|
+
async getRequest(requestId) {
|
|
25
|
+
const res = await fetch(`${this.baseUrl}/api/v1/requests/${requestId}`, {
|
|
26
|
+
headers: this.headers(),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`getRequest failed: ${res.status}`);
|
|
30
|
+
return res.json();
|
|
31
|
+
}
|
|
24
32
|
async getPendingRequests() {
|
|
25
33
|
const res = await fetch(`${this.baseUrl}/api/v1/requests/pending`, {
|
|
26
34
|
headers: this.headers(),
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { AskMeshClient } from './client/askmesh_client.js';
|
|
5
5
|
import { SseListener } from './sse/sse_listener.js';
|
|
6
6
|
import { AutoResponder } from './agent/auto_responder.js';
|
|
7
|
-
import {
|
|
8
|
-
import { registerListAgents } from './tools/list_agents.js';
|
|
9
|
-
import { registerGetStatus } from './tools/get_status.js';
|
|
10
|
-
import { registerAnswerPending } from './tools/answer_pending.js';
|
|
11
|
-
import { registerSetContext } from './tools/set_context.js';
|
|
7
|
+
import { registerAskMesh } from './tools/askmesh.js';
|
|
12
8
|
const TOKEN = process.env.ASKMESH_TOKEN;
|
|
13
9
|
const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
|
|
14
10
|
if (!TOKEN) {
|
|
@@ -19,19 +15,18 @@ const client = new AskMeshClient(URL, TOKEN);
|
|
|
19
15
|
const autoResponder = new AutoResponder(client);
|
|
20
16
|
const server = new McpServer({
|
|
21
17
|
name: 'askmesh',
|
|
22
|
-
version: '0.
|
|
18
|
+
version: '0.3.0',
|
|
23
19
|
});
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
registerSetContext(server, client);
|
|
30
|
-
// Start SSE listener — auto-respond to incoming questions
|
|
20
|
+
// Single unified tool
|
|
21
|
+
registerAskMesh(server, client);
|
|
22
|
+
// Give the auto-responder access to the underlying MCP server for sampling
|
|
23
|
+
autoResponder.setServer(server.server);
|
|
24
|
+
// Start SSE listener — auto-respond to incoming questions + receive answers
|
|
31
25
|
const sse = new SseListener();
|
|
32
26
|
sse.start(URL, TOKEN, async (request) => {
|
|
33
|
-
console.error(`[AskMesh] Incoming request from @${request.fromUsername}: "${request.question}"`);
|
|
34
27
|
await autoResponder.handleRequest(request);
|
|
28
|
+
}, (answer) => {
|
|
29
|
+
console.error(`[AskMesh] Answer received for request #${answer.id}: "${answer.answer.slice(0, 100)}${answer.answer.length > 100 ? '...' : ''}"`);
|
|
35
30
|
});
|
|
36
31
|
// Cleanup on exit
|
|
37
32
|
process.on('SIGINT', () => {
|
|
@@ -5,8 +5,12 @@ export interface IncomingRequest {
|
|
|
5
5
|
question: string;
|
|
6
6
|
context: string | null;
|
|
7
7
|
}
|
|
8
|
+
export interface IncomingAnswer {
|
|
9
|
+
id: number;
|
|
10
|
+
answer: string;
|
|
11
|
+
}
|
|
8
12
|
export declare class SseListener {
|
|
9
13
|
private es;
|
|
10
|
-
start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void): void;
|
|
14
|
+
start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void, onAnswer?: (ans: IncomingAnswer) => void): void;
|
|
11
15
|
stop(): void;
|
|
12
16
|
}
|
package/dist/sse/sse_listener.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventSource from 'eventsource';
|
|
2
2
|
export class SseListener {
|
|
3
3
|
es = null;
|
|
4
|
-
start(baseUrl, token, onRequest) {
|
|
4
|
+
start(baseUrl, token, onRequest, onAnswer) {
|
|
5
5
|
this.es = new EventSource(`${baseUrl}/api/v1/sse/subscribe`, {
|
|
6
6
|
headers: { Authorization: `Bearer ${token}` },
|
|
7
7
|
});
|
|
@@ -10,15 +10,19 @@ export class SseListener {
|
|
|
10
10
|
const payload = JSON.parse(e.data);
|
|
11
11
|
onRequest(payload);
|
|
12
12
|
}
|
|
13
|
-
catch {
|
|
14
|
-
|
|
13
|
+
catch { }
|
|
14
|
+
}));
|
|
15
|
+
this.es.addEventListener('answer_ready', ((e) => {
|
|
16
|
+
try {
|
|
17
|
+
const payload = JSON.parse(e.data);
|
|
18
|
+
if (onAnswer)
|
|
19
|
+
onAnswer(payload);
|
|
20
|
+
console.error(`[AskMesh] Answer received for request #${payload.id}`);
|
|
15
21
|
}
|
|
22
|
+
catch { }
|
|
16
23
|
}));
|
|
17
|
-
this.es.addEventListener('ping', () => {
|
|
18
|
-
// keepalive — nothing to do
|
|
19
|
-
});
|
|
24
|
+
this.es.addEventListener('ping', () => { });
|
|
20
25
|
this.es.onerror = () => {
|
|
21
|
-
// EventSource handles automatic reconnection
|
|
22
26
|
console.error('[AskMesh SSE] Connection error — will reconnect');
|
|
23
27
|
};
|
|
24
28
|
}
|
package/dist/tools/ask_agent.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export function registerAskAgent(server, client) {
|
|
3
|
-
server.tool('ask_agent', 'Pose une question à un agent de ton équipe
|
|
3
|
+
server.tool('ask_agent', 'Pose une question à un agent de ton équipe. Attend la réponse (max 60s).', {
|
|
4
4
|
username: z.string().describe("Username de l'agent cible (ex: manu)"),
|
|
5
5
|
question: z.string().describe('La question à poser'),
|
|
6
6
|
context: z.string().optional().describe('Contexte additionnel (optionnel)'),
|
|
7
7
|
}, async ({ username, question, context }) => {
|
|
8
8
|
const target = username.replace(/^@/, '');
|
|
9
9
|
const result = await client.askAgent(target, question, context);
|
|
10
|
+
const requestId = result.requestId;
|
|
11
|
+
// Poll for the answer (max 60s, check every 3s)
|
|
12
|
+
for (let i = 0; i < 20; i++) {
|
|
13
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
14
|
+
try {
|
|
15
|
+
const req = await client.getRequest(requestId);
|
|
16
|
+
if (req.status === 'answered' && req.answer) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
text: `@${target} a répondu :\n\n${req.answer}`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
10
29
|
return {
|
|
11
30
|
content: [
|
|
12
31
|
{
|
|
13
32
|
type: 'text',
|
|
14
|
-
text: `Question envoyée à @${target} (request #${
|
|
33
|
+
text: `Question envoyée à @${target} (request #${requestId}). L'agent n'a pas encore répondu (timeout 60s). La réponse arrivera plus tard — vérifie avec answer_pending.`,
|
|
15
34
|
},
|
|
16
35
|
],
|
|
17
36
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerAskMesh(server, client) {
|
|
3
|
+
server.tool('askmesh', `AskMesh — communique avec les agents de ton équipe.
|
|
4
|
+
|
|
5
|
+
Actions:
|
|
6
|
+
- "ask" : pose une question à un agent (attend la réponse max 60s)
|
|
7
|
+
- "list" : liste les agents de tes teams et leur statut
|
|
8
|
+
- "status" : vérifie si un agent est disponible
|
|
9
|
+
- "pending" : voir les questions qu'on t'a posées
|
|
10
|
+
- "answer" : répondre à une question en attente
|
|
11
|
+
- "context" : partager ton contexte projet avec ta team`, {
|
|
12
|
+
action: z.enum(['ask', 'list', 'status', 'pending', 'answer', 'context']).describe('Action à effectuer'),
|
|
13
|
+
username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
|
|
14
|
+
question: z.string().optional().describe('Question à poser (pour ask)'),
|
|
15
|
+
requestId: z.number().optional().describe('ID de la requête (pour answer)'),
|
|
16
|
+
message: z.string().optional().describe('Réponse ou contexte à envoyer (pour answer/context)'),
|
|
17
|
+
}, async ({ action, username, question, requestId, message }) => {
|
|
18
|
+
switch (action) {
|
|
19
|
+
case 'ask': {
|
|
20
|
+
if (!username || !question) {
|
|
21
|
+
return text("Paramètres requis : username et question");
|
|
22
|
+
}
|
|
23
|
+
const target = username.replace(/^@/, '');
|
|
24
|
+
const result = await client.askAgent(target, question);
|
|
25
|
+
const reqId = result.requestId;
|
|
26
|
+
// Poll for answer (max 60s)
|
|
27
|
+
for (let i = 0; i < 20; i++) {
|
|
28
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
29
|
+
try {
|
|
30
|
+
const req = await client.getRequest(reqId);
|
|
31
|
+
if (req.status === 'answered' && req.answer) {
|
|
32
|
+
return text(`@${target} a répondu :\n\n${req.answer}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
return text(`Question envoyée à @${target} (#${reqId}). Pas de réponse dans les 60s — utilise "pending" pour vérifier plus tard.`);
|
|
38
|
+
}
|
|
39
|
+
case 'list': {
|
|
40
|
+
const { agents } = await client.listAgents();
|
|
41
|
+
if (agents.length === 0)
|
|
42
|
+
return text('Aucun agent dans tes teams.');
|
|
43
|
+
const lines = agents.map((a) => {
|
|
44
|
+
const icon = a.status === 'online' ? '🟢' : '⚫';
|
|
45
|
+
return `${icon} @${a.username} — ${a.status}`;
|
|
46
|
+
});
|
|
47
|
+
return text(lines.join('\n'));
|
|
48
|
+
}
|
|
49
|
+
case 'status': {
|
|
50
|
+
if (!username)
|
|
51
|
+
return text("Paramètre requis : username");
|
|
52
|
+
const target = username.replace(/^@/, '');
|
|
53
|
+
const result = await client.getAgentStatus(target);
|
|
54
|
+
const icon = result.status === 'online' ? '🟢' : '⚫';
|
|
55
|
+
return text(`${icon} @${result.username} — ${result.status}${result.lastSeenAt ? `\nDernière activité: ${result.lastSeenAt}` : ''}`);
|
|
56
|
+
}
|
|
57
|
+
case 'pending': {
|
|
58
|
+
const { requests } = await client.getPendingRequests();
|
|
59
|
+
if (requests.length === 0)
|
|
60
|
+
return text('Aucune question en attente.');
|
|
61
|
+
const lines = requests.map((r) => `#${r.id} — "${r.question}"`);
|
|
62
|
+
return text(`Questions en attente:\n${lines.join('\n')}\n\nUtilise action "answer" avec requestId et message pour répondre.`);
|
|
63
|
+
}
|
|
64
|
+
case 'answer': {
|
|
65
|
+
if (!requestId || !message) {
|
|
66
|
+
return text("Paramètres requis : requestId et message");
|
|
67
|
+
}
|
|
68
|
+
const result = await client.answerRequest(requestId, message);
|
|
69
|
+
return text(`Réponse envoyée pour la requête #${result.id}.`);
|
|
70
|
+
}
|
|
71
|
+
case 'context': {
|
|
72
|
+
if (!message)
|
|
73
|
+
return text("Paramètre requis : message (le contenu du contexte)");
|
|
74
|
+
await client.setContext(message);
|
|
75
|
+
return text(`Contexte mis à jour (${message.length} caractères).`);
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return text('Action inconnue. Actions disponibles : ask, list, status, pending, answer, context');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function text(t) {
|
|
83
|
+
return { content: [{ type: 'text', text: t }] };
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function readLocalContext(): string;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
export function readLocalContext() {
|
|
5
|
-
const parts = [];
|
|
6
|
-
// 1. Read CLAUDE.md from current working directory
|
|
7
|
-
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
8
|
-
if (existsSync(claudeMdPath)) {
|
|
9
|
-
parts.push('=== CLAUDE.md (project context) ===');
|
|
10
|
-
parts.push(readSafe(claudeMdPath));
|
|
11
|
-
}
|
|
12
|
-
// 2. Read Claude Code memories
|
|
13
|
-
const memoryDir = join(homedir(), '.claude', 'memory');
|
|
14
|
-
if (existsSync(memoryDir)) {
|
|
15
|
-
try {
|
|
16
|
-
const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'));
|
|
17
|
-
if (files.length > 0) {
|
|
18
|
-
parts.push('=== Claude Code memories ===');
|
|
19
|
-
for (const file of files) {
|
|
20
|
-
const content = readSafe(join(memoryDir, file));
|
|
21
|
-
if (content) {
|
|
22
|
-
parts.push(`--- ${file} ---`);
|
|
23
|
-
parts.push(content);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch { }
|
|
29
|
-
}
|
|
30
|
-
// 3. Read project-specific memories
|
|
31
|
-
const cwd = process.cwd();
|
|
32
|
-
const projectMemoryDir = join(homedir(), '.claude', 'projects');
|
|
33
|
-
if (existsSync(projectMemoryDir)) {
|
|
34
|
-
try {
|
|
35
|
-
// Claude Code stores project memories in a hashed directory
|
|
36
|
-
const dirs = readdirSync(projectMemoryDir);
|
|
37
|
-
for (const dir of dirs) {
|
|
38
|
-
// Check if this directory matches our project
|
|
39
|
-
if (dir.includes(cwd.replace(/\//g, '-').replace(/^-/, ''))) {
|
|
40
|
-
const memDir = join(projectMemoryDir, dir, 'memory');
|
|
41
|
-
if (existsSync(memDir)) {
|
|
42
|
-
const files = readdirSync(memDir).filter((f) => f.endsWith('.md'));
|
|
43
|
-
if (files.length > 0) {
|
|
44
|
-
parts.push('=== Project-specific memories ===');
|
|
45
|
-
for (const file of files) {
|
|
46
|
-
const content = readSafe(join(memDir, file));
|
|
47
|
-
if (content) {
|
|
48
|
-
parts.push(`--- ${file} ---`);
|
|
49
|
-
parts.push(content);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
}
|
|
59
|
-
return parts.join('\n\n') || 'No local context available.';
|
|
60
|
-
}
|
|
61
|
-
function readSafe(path) {
|
|
62
|
-
try {
|
|
63
|
-
return readFileSync(path, 'utf-8').trim();
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
68
|
-
}
|