@askmesh/mcp 0.2.1 → 0.3.1

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 to `~/.claude/settings.json`:
15
+ Add `.mcp.json` at the root of your project:
16
16
 
17
17
  ```json
18
18
  {
@@ -22,40 +22,51 @@ 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 5 tools appear.
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
- ## Tools
39
+ ## Usage
41
40
 
42
- | Tool | Description |
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
- | `ask_agent` | Ask a question to a teammate's agent |
45
- | `list_agents` | See who's online in your team |
46
- | `get_status` | Check if a specific agent is available |
47
- | `answer_pending` | List and respond to incoming questions |
48
- | `set_context` | Share your project context with your team |
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 `ANTHROPIC_API_KEY` is set, your agent automatically answers incoming questions using:
53
-
54
- - Your **CLAUDE.md** (project context)
55
- - Your **Claude Code memories** (`~/.claude/memory/`)
56
- - Your **project-specific memories**
65
+ Questions are answered automatically using 3 strategies (in order):
57
66
 
58
- Without the API key, questions are queued and you respond manually via `answer_pending`.
67
+ 1. **MCP Sampling** asks your active Claude Code to respond (no extra config)
68
+ 2. **Anthropic API** — reads your CLAUDE.md + memories, calls Claude API (requires `ANTHROPIC_API_KEY`)
69
+ 3. **Manual** — question stays pending, respond via `askmesh(action:"pending")`
59
70
 
60
71
  ## Environment variables
61
72
 
@@ -63,7 +74,7 @@ Without the API key, questions are queued and you respond manually via `answer_p
63
74
  |---|---|---|
64
75
  | `ASKMESH_TOKEN` | Yes | Your agent API token from askmesh.dev |
65
76
  | `ASKMESH_URL` | No | API URL (default: `https://api.askmesh.dev`) |
66
- | `ANTHROPIC_API_KEY` | No | Enables auto-responses via Claude API |
77
+ | `ANTHROPIC_API_KEY` | No | Enables auto-responses via Claude API when MCP sampling unavailable |
67
78
 
68
79
  ## How it works
69
80
 
@@ -75,9 +86,8 @@ You (Claude Code) AskMesh Cloud Teammate (Claude Code)
75
86
  | |<--- ask @you "question" |
76
87
  |<-- SSE: request --------| |
77
88
  | | |
78
- | [reads CLAUDE.md + | |
79
- | memories, calls | |
80
- | Claude API] | |
89
+ | [auto-respond using | |
90
+ | CLAUDE.md + memories] | |
81
91
  | | |
82
92
  |--- answer ------------->|--- SSE: answer -------->|
83
93
  ```
@@ -7,4 +7,5 @@ export declare class AutoResponder {
7
7
  constructor(client: AskMeshClient);
8
8
  setServer(server: Server): void;
9
9
  handleRequest(request: IncomingRequest): Promise<void>;
10
+ private callAnthropicAPI;
10
11
  }
@@ -1,3 +1,4 @@
1
+ import { readLocalContext } from './context_reader.js';
1
2
  export class AutoResponder {
2
3
  client;
3
4
  mcpServer = null;
@@ -9,7 +10,7 @@ export class AutoResponder {
9
10
  }
10
11
  async handleRequest(request) {
11
12
  console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
12
- // Try MCP sampling — asks the active Claude Code to generate a response
13
+ // Strategy 1: MCP sampling — asks the active Claude Code to respond
13
14
  if (this.mcpServer) {
14
15
  try {
15
16
  const result = (await this.mcpServer.request({
@@ -38,14 +39,63 @@ export class AutoResponder {
38
39
  const answer = result?.content?.text || result?.content?.[0]?.text;
39
40
  if (answer) {
40
41
  await this.client.answerRequest(request.id, answer);
41
- console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
42
+ console.error(`[AskMesh] Responded to #${request.id} via MCP sampling`);
42
43
  return;
43
44
  }
44
45
  }
45
46
  catch {
46
- console.error('[AskMesh] Sampling not available question queued');
47
+ console.error('[AskMesh] MCP sampling not available, trying API fallback...');
47
48
  }
48
49
  }
49
- console.error(`[AskMesh] Question queued use answer_pending to respond`);
50
+ // Strategy 2: Direct Anthropic API with local context
51
+ const apiKey = process.env.ANTHROPIC_API_KEY;
52
+ if (apiKey) {
53
+ try {
54
+ const context = readLocalContext();
55
+ const answer = await this.callAnthropicAPI(apiKey, request, context);
56
+ if (answer) {
57
+ await this.client.answerRequest(request.id, answer);
58
+ console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
59
+ return;
60
+ }
61
+ }
62
+ catch (err) {
63
+ console.error('[AskMesh] Anthropic API failed:', err);
64
+ }
65
+ }
66
+ // Strategy 3: Manual — question stays pending
67
+ console.error(`[AskMesh] Question #${request.id} queued — use askmesh(action:"pending") to see and respond`);
68
+ }
69
+ async callAnthropicAPI(apiKey, request, context) {
70
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
71
+ method: 'POST',
72
+ headers: {
73
+ 'x-api-key': apiKey,
74
+ 'anthropic-version': '2023-06-01',
75
+ 'content-type': 'application/json',
76
+ },
77
+ body: JSON.stringify({
78
+ model: 'claude-sonnet-4-20250514',
79
+ max_tokens: 2048,
80
+ system: `You are an AI coding agent responding on behalf of a developer through AskMesh.
81
+ You have access to the developer's project context below.
82
+ Use this context to answer the question accurately and concisely.
83
+ Reference specific files, conventions, or decisions from the context when relevant.
84
+ If the context doesn't contain enough info, say so honestly.
85
+ Answer in the same language as the question.
86
+ Keep responses under 500 words unless more detail is needed.`,
87
+ messages: [
88
+ {
89
+ role: 'user',
90
+ content: `Project context:\n\n${context}\n\n---\n\nQuestion from @${request.fromUsername}:\n${request.question}${request.context ? `\n\nAdditional context: ${request.context}` : ''}`,
91
+ },
92
+ ],
93
+ }),
94
+ });
95
+ if (!response.ok) {
96
+ throw new Error(`Anthropic API ${response.status}: ${await response.text()}`);
97
+ }
98
+ const data = (await response.json());
99
+ return data.content?.[0]?.text || null;
50
100
  }
51
101
  }
@@ -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
+ }
@@ -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 { registerAskAgent } from './tools/ask_agent.js';
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,20 +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.2.0',
18
+ version: '0.3.0',
23
19
  });
24
- // Register all tools
25
- registerAskAgent(server, client);
26
- registerListAgents(server, client);
27
- registerGetStatus(server, client);
28
- registerAnswerPending(server, client);
29
- registerSetContext(server, client);
20
+ // Single unified tool
21
+ registerAskMesh(server, client);
30
22
  // Give the auto-responder access to the underlying MCP server for sampling
31
23
  autoResponder.setServer(server.server);
32
- // Start SSE listener — auto-respond to incoming questions
24
+ // Start SSE listener — auto-respond to incoming questions + receive answers
33
25
  const sse = new SseListener();
34
26
  sse.start(URL, TOKEN, async (request) => {
35
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 ? '...' : ''}"`);
36
30
  });
37
31
  // Cleanup on exit
38
32
  process.on('SIGINT', () => {
@@ -5,8 +5,20 @@ 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
+ private reconnectTimer;
15
+ private baseUrl;
16
+ private token;
17
+ private onRequest;
18
+ private onAnswer;
19
+ private connected;
20
+ start(baseUrl: string, token: string, onRequest: (req: IncomingRequest) => void, onAnswer?: (ans: IncomingAnswer) => void): void;
21
+ private connect;
22
+ private scheduleReconnect;
11
23
  stop(): void;
12
24
  }
@@ -1,29 +1,77 @@
1
1
  import EventSource from 'eventsource';
2
2
  export class SseListener {
3
3
  es = null;
4
- start(baseUrl, token, onRequest) {
5
- this.es = new EventSource(`${baseUrl}/api/v1/sse/subscribe`, {
6
- headers: { Authorization: `Bearer ${token}` },
4
+ reconnectTimer = null;
5
+ baseUrl = '';
6
+ token = '';
7
+ onRequest = null;
8
+ onAnswer = null;
9
+ connected = false;
10
+ start(baseUrl, token, onRequest, onAnswer) {
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
- catch {
14
- // ignore parse errors
34
+ catch { }
35
+ }));
36
+ this.es.addEventListener('answer_ready', ((e) => {
37
+ try {
38
+ const payload = JSON.parse(e.data);
39
+ this.onAnswer?.(payload);
40
+ console.error(`[AskMesh] Answer received for request #${payload.id}`);
15
41
  }
42
+ catch { }
16
43
  }));
17
44
  this.es.addEventListener('ping', () => {
18
- // keepalivenothing to do
45
+ // Keepaliveconfirms connection is alive
19
46
  });
20
47
  this.es.onerror = () => {
21
- // EventSource handles automatic reconnection
22
- console.error('[AskMesh SSE] Connection error — will reconnect');
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();
23
55
  };
24
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
+ }
25
68
  stop() {
69
+ if (this.reconnectTimer) {
70
+ clearTimeout(this.reconnectTimer);
71
+ this.reconnectTimer = null;
72
+ }
26
73
  this.es?.close();
27
74
  this.es = null;
75
+ this.connected = false;
28
76
  }
29
77
  }
@@ -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 par son username', {
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 #${result.requestId})\nStatut: ${result.status}`,
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,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { AskMeshClient } from '../client/askmesh_client.js';
3
+ export declare function registerAskMesh(server: McpServer, client: AskMeshClient): void;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@askmesh/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "AskMesh MCP server — connect your AI coding agent to your team's mesh network",
5
5
  "type": "module",
6
6
  "bin": {