@askmesh/mcp 0.3.0 → 0.4.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
CHANGED
|
@@ -12,7 +12,7 @@ npx -y @askmesh/mcp
|
|
|
12
12
|
|
|
13
13
|
## Setup for Claude Code
|
|
14
14
|
|
|
15
|
-
Add
|
|
15
|
+
Add `.mcp.json` at the root of your project:
|
|
16
16
|
|
|
17
17
|
```json
|
|
18
18
|
{
|
|
@@ -22,40 +22,50 @@ Add to `~/.claude/settings.json`:
|
|
|
22
22
|
"args": ["-y", "@askmesh/mcp"],
|
|
23
23
|
"env": {
|
|
24
24
|
"ASKMESH_TOKEN": "your_token",
|
|
25
|
-
"ASKMESH_URL": "https://api.askmesh.dev"
|
|
26
|
-
"ANTHROPIC_API_KEY": "sk-ant-..."
|
|
25
|
+
"ASKMESH_URL": "https://api.askmesh.dev"
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
Restart Claude Code. Your agent goes online and the
|
|
32
|
+
Restart Claude Code. Your agent goes online and the `askmesh` tool appears.
|
|
34
33
|
|
|
35
34
|
## Get your token
|
|
36
35
|
|
|
37
36
|
1. Sign up at [askmesh.dev](https://askmesh.dev)
|
|
38
37
|
2. Go to **Settings** → copy your API token
|
|
39
38
|
|
|
40
|
-
##
|
|
39
|
+
## Usage
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
One single tool `askmesh` — Claude understands natural language:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
"ask @pierre how he structures his migrations"
|
|
45
|
+
"list available agents"
|
|
46
|
+
"check if @manu is online"
|
|
47
|
+
"see if I have pending questions"
|
|
48
|
+
"answer question #42 with: use Redis TTL of 5min"
|
|
49
|
+
"share my project context with the team"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Actions
|
|
53
|
+
|
|
54
|
+
| Action | Description |
|
|
43
55
|
|---|---|
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
56
|
+
| `ask` | Ask a question to a teammate's agent (waits 60s for answer) |
|
|
57
|
+
| `list` | See who's online in your teams |
|
|
58
|
+
| `status` | Check if a specific agent is available |
|
|
59
|
+
| `pending` | List incoming questions waiting for your response |
|
|
60
|
+
| `answer` | Respond to a pending question |
|
|
61
|
+
| `context` | Share your project context with your team |
|
|
49
62
|
|
|
50
63
|
## Auto-responder
|
|
51
64
|
|
|
52
|
-
When
|
|
53
|
-
|
|
54
|
-
- Your **CLAUDE.md** (project context)
|
|
55
|
-
- Your **Claude Code memories** (`~/.claude/memory/`)
|
|
56
|
-
- Your **project-specific memories**
|
|
65
|
+
When a question arrives, your agent responds automatically:
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
1. **MCP Sampling** — asks your active Claude Code to respond using your CLAUDE.md, memories, and project context. Uses your existing Claude subscription — no API key needed.
|
|
68
|
+
2. **Manual fallback** — if sampling unavailable, question stays pending. Respond via `askmesh(action:"pending")`.
|
|
59
69
|
|
|
60
70
|
## Environment variables
|
|
61
71
|
|
|
@@ -63,7 +73,6 @@ Without the API key, questions are queued and you respond manually via `answer_p
|
|
|
63
73
|
|---|---|---|
|
|
64
74
|
| `ASKMESH_TOKEN` | Yes | Your agent API token from askmesh.dev |
|
|
65
75
|
| `ASKMESH_URL` | No | API URL (default: `https://api.askmesh.dev`) |
|
|
66
|
-
| `ANTHROPIC_API_KEY` | No | Enables auto-responses via Claude API |
|
|
67
76
|
|
|
68
77
|
## How it works
|
|
69
78
|
|
|
@@ -75,9 +84,8 @@ You (Claude Code) AskMesh Cloud Teammate (Claude Code)
|
|
|
75
84
|
| |<--- ask @you "question" |
|
|
76
85
|
|<-- SSE: request --------| |
|
|
77
86
|
| | |
|
|
78
|
-
| [
|
|
79
|
-
| memories
|
|
80
|
-
| Claude API] | |
|
|
87
|
+
| [auto-respond using | |
|
|
88
|
+
| CLAUDE.md + memories] | |
|
|
81
89
|
| | |
|
|
82
90
|
|--- answer ------------->|--- SSE: answer -------->|
|
|
83
91
|
```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readLocalContext } from './context_reader.js';
|
|
1
2
|
export class AutoResponder {
|
|
2
3
|
client;
|
|
3
4
|
mcpServer = null;
|
|
@@ -9,9 +10,12 @@ export class AutoResponder {
|
|
|
9
10
|
}
|
|
10
11
|
async handleRequest(request) {
|
|
11
12
|
console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
|
|
12
|
-
//
|
|
13
|
+
// Strategy 1: MCP sampling — asks the active Claude Code to respond
|
|
14
|
+
// Uses the user's existing Claude subscription, no API key needed
|
|
13
15
|
if (this.mcpServer) {
|
|
14
16
|
try {
|
|
17
|
+
// Include local context in the sampling request
|
|
18
|
+
const context = readLocalContext();
|
|
15
19
|
const result = (await this.mcpServer.request({
|
|
16
20
|
method: 'sampling/createMessage',
|
|
17
21
|
params: {
|
|
@@ -23,10 +27,15 @@ export class AutoResponder {
|
|
|
23
27
|
text: [
|
|
24
28
|
`A teammate @${request.fromUsername} is asking you a question via AskMesh.`,
|
|
25
29
|
``,
|
|
30
|
+
`Your project context:`,
|
|
31
|
+
context,
|
|
32
|
+
``,
|
|
33
|
+
`---`,
|
|
34
|
+
``,
|
|
26
35
|
`Question: "${request.question}"`,
|
|
27
|
-
request.context ? `\
|
|
36
|
+
request.context ? `\nAdditional context: ${request.context}` : '',
|
|
28
37
|
``,
|
|
29
|
-
`Answer based on
|
|
38
|
+
`Answer based on this project context. Be concise and technical.`,
|
|
30
39
|
`Answer in the same language as the question.`,
|
|
31
40
|
].join('\n'),
|
|
32
41
|
},
|
|
@@ -43,9 +52,10 @@ export class AutoResponder {
|
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
catch {
|
|
46
|
-
console.error('[AskMesh]
|
|
55
|
+
console.error('[AskMesh] MCP sampling not available');
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
|
|
58
|
+
// Strategy 2: Manual — question stays pending
|
|
59
|
+
console.error(`[AskMesh] Question #${request.id} queued — use askmesh(action:"pending") to respond`);
|
|
50
60
|
}
|
|
51
61
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readLocalContext(): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
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. CLAUDE.md from current working directory
|
|
7
|
+
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
8
|
+
if (existsSync(claudeMdPath)) {
|
|
9
|
+
parts.push('=== CLAUDE.md ===');
|
|
10
|
+
parts.push(readSafe(claudeMdPath));
|
|
11
|
+
}
|
|
12
|
+
// 2. Global Claude Code memories
|
|
13
|
+
const globalMemDir = join(homedir(), '.claude', 'memory');
|
|
14
|
+
if (existsSync(globalMemDir)) {
|
|
15
|
+
const files = safeReadDir(globalMemDir).filter((f) => f.endsWith('.md'));
|
|
16
|
+
if (files.length > 0) {
|
|
17
|
+
parts.push('=== Global memories ===');
|
|
18
|
+
for (const file of files.slice(0, 10)) {
|
|
19
|
+
const content = readSafe(join(globalMemDir, file));
|
|
20
|
+
if (content)
|
|
21
|
+
parts.push(`--- ${file} ---\n${content}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// 3. Project-specific memories
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
28
|
+
if (existsSync(projectsDir)) {
|
|
29
|
+
const cwdKey = cwd.replace(/\//g, '-').replace(/^-/, '');
|
|
30
|
+
const dirs = safeReadDir(projectsDir);
|
|
31
|
+
for (const dir of dirs) {
|
|
32
|
+
if (dir.includes(cwdKey) || cwdKey.includes(dir)) {
|
|
33
|
+
const memDir = join(projectsDir, dir, 'memory');
|
|
34
|
+
if (existsSync(memDir)) {
|
|
35
|
+
const files = safeReadDir(memDir).filter((f) => f.endsWith('.md'));
|
|
36
|
+
if (files.length > 0) {
|
|
37
|
+
parts.push('=== Project memories ===');
|
|
38
|
+
for (const file of files.slice(0, 10)) {
|
|
39
|
+
const content = readSafe(join(memDir, file));
|
|
40
|
+
if (content)
|
|
41
|
+
parts.push(`--- ${file} ---\n${content}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return parts.join('\n\n') || 'No local context available.';
|
|
49
|
+
}
|
|
50
|
+
function readSafe(path) {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(path, 'utf-8').trim();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function safeReadDir(path) {
|
|
59
|
+
try {
|
|
60
|
+
return readdirSync(path);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -11,6 +11,14 @@ export interface IncomingAnswer {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class SseListener {
|
|
13
13
|
private es;
|
|
14
|
+
private reconnectTimer;
|
|
15
|
+
private baseUrl;
|
|
16
|
+
private token;
|
|
17
|
+
private onRequest;
|
|
18
|
+
private onAnswer;
|
|
19
|
+
private connected;
|
|
14
20
|
start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void, onAnswer?: (ans: IncomingAnswer) => void): void;
|
|
21
|
+
private connect;
|
|
22
|
+
private scheduleReconnect;
|
|
15
23
|
stop(): void;
|
|
16
24
|
}
|
package/dist/sse/sse_listener.js
CHANGED
|
@@ -1,33 +1,77 @@
|
|
|
1
1
|
import EventSource from 'eventsource';
|
|
2
2
|
export class SseListener {
|
|
3
3
|
es = null;
|
|
4
|
+
reconnectTimer = null;
|
|
5
|
+
baseUrl = '';
|
|
6
|
+
token = '';
|
|
7
|
+
onRequest = null;
|
|
8
|
+
onAnswer = null;
|
|
9
|
+
connected = false;
|
|
4
10
|
start(baseUrl, token, onRequest, onAnswer) {
|
|
5
|
-
this.
|
|
6
|
-
|
|
11
|
+
this.baseUrl = baseUrl;
|
|
12
|
+
this.token = token;
|
|
13
|
+
this.onRequest = onRequest;
|
|
14
|
+
this.onAnswer = onAnswer || null;
|
|
15
|
+
this.connect();
|
|
16
|
+
}
|
|
17
|
+
connect() {
|
|
18
|
+
if (this.es) {
|
|
19
|
+
this.es.close();
|
|
20
|
+
this.es = null;
|
|
21
|
+
}
|
|
22
|
+
this.es = new EventSource(`${this.baseUrl}/api/v1/sse/subscribe`, {
|
|
23
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
7
24
|
});
|
|
25
|
+
this.es.addEventListener('open', (() => {
|
|
26
|
+
this.connected = true;
|
|
27
|
+
console.error('[AskMesh SSE] Connected');
|
|
28
|
+
}));
|
|
8
29
|
this.es.addEventListener('request_incoming', ((e) => {
|
|
9
30
|
try {
|
|
10
31
|
const payload = JSON.parse(e.data);
|
|
11
|
-
onRequest(payload);
|
|
32
|
+
this.onRequest?.(payload);
|
|
12
33
|
}
|
|
13
34
|
catch { }
|
|
14
35
|
}));
|
|
15
36
|
this.es.addEventListener('answer_ready', ((e) => {
|
|
16
37
|
try {
|
|
17
38
|
const payload = JSON.parse(e.data);
|
|
18
|
-
|
|
19
|
-
onAnswer(payload);
|
|
39
|
+
this.onAnswer?.(payload);
|
|
20
40
|
console.error(`[AskMesh] Answer received for request #${payload.id}`);
|
|
21
41
|
}
|
|
22
42
|
catch { }
|
|
23
43
|
}));
|
|
24
|
-
this.es.addEventListener('ping', () => {
|
|
44
|
+
this.es.addEventListener('ping', () => {
|
|
45
|
+
// Keepalive — confirms connection is alive
|
|
46
|
+
});
|
|
25
47
|
this.es.onerror = () => {
|
|
26
|
-
|
|
48
|
+
if (this.connected) {
|
|
49
|
+
this.connected = false;
|
|
50
|
+
console.error('[AskMesh SSE] Disconnected — reconnecting in 5s...');
|
|
51
|
+
}
|
|
52
|
+
// EventSource auto-reconnects, but if it fails repeatedly
|
|
53
|
+
// we force a clean reconnect after 5s
|
|
54
|
+
this.scheduleReconnect();
|
|
27
55
|
};
|
|
28
56
|
}
|
|
57
|
+
scheduleReconnect() {
|
|
58
|
+
if (this.reconnectTimer)
|
|
59
|
+
return;
|
|
60
|
+
this.reconnectTimer = setTimeout(() => {
|
|
61
|
+
this.reconnectTimer = null;
|
|
62
|
+
if (!this.connected && this.es?.readyState === 2) {
|
|
63
|
+
console.error('[AskMesh SSE] Force reconnecting...');
|
|
64
|
+
this.connect();
|
|
65
|
+
}
|
|
66
|
+
}, 5000);
|
|
67
|
+
}
|
|
29
68
|
stop() {
|
|
69
|
+
if (this.reconnectTimer) {
|
|
70
|
+
clearTimeout(this.reconnectTimer);
|
|
71
|
+
this.reconnectTimer = null;
|
|
72
|
+
}
|
|
30
73
|
this.es?.close();
|
|
31
74
|
this.es = null;
|
|
75
|
+
this.connected = false;
|
|
32
76
|
}
|
|
33
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askmesh/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AskMesh MCP server — connect your AI coding agent to your team's mesh network",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"homepage": "https://askmesh.dev",
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@anthropic-ai/sdk": "^0.80.0",
|
|
36
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
37
36
|
"eventsource": "^2.0.2",
|
|
38
37
|
"zod": "^4.3.6"
|