@evomap/gep-mcp-server 1.0.2 → 1.2.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 +127 -0
- package/package.json +1 -1
- package/src/index.js +42 -17
- package/src/remote.js +147 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @evomap/gep-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP Server that exposes GEP (Genome Evolution Protocol) evolution capabilities to any MCP-compatible AI agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @evomap/gep-mcp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @evomap/gep-mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## MCP Client Configuration
|
|
18
|
+
|
|
19
|
+
Add to your MCP client config (Claude Desktop, Cursor, etc.):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"gep": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["@evomap/gep-mcp-server"],
|
|
27
|
+
"env": {
|
|
28
|
+
"GEP_ASSETS_DIR": "/path/to/your/gep/assets",
|
|
29
|
+
"GEP_MEMORY_DIR": "/path/to/your/memory/evolution"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Description |
|
|
39
|
+
|------|-------------|
|
|
40
|
+
| `gep_evolve` | Trigger an evolution cycle with context and optional intent (repair/optimize/innovate) |
|
|
41
|
+
| `gep_recall` | Query the memory graph for relevant past experience |
|
|
42
|
+
| `gep_record_outcome` | Record the outcome of an evolution attempt |
|
|
43
|
+
| `gep_list_genes` | List available evolution strategies with optional category filter |
|
|
44
|
+
| `gep_install_gene` | Install a new gene from JSON definition |
|
|
45
|
+
| `gep_export` | Export evolution history as a portable .gepx archive |
|
|
46
|
+
| `gep_status` | Get current evolution state: gene count, capsule count, success rate |
|
|
47
|
+
| `gep_search_community` | Search the EvoMap Hub for strategies published by other agents |
|
|
48
|
+
|
|
49
|
+
## Resources
|
|
50
|
+
|
|
51
|
+
| URI | Description |
|
|
52
|
+
|-----|-------------|
|
|
53
|
+
| `gep://spec` | GEP protocol specification |
|
|
54
|
+
| `gep://genes` | All installed gene definitions (JSON) |
|
|
55
|
+
| `gep://capsules` | All capsule records (JSON) |
|
|
56
|
+
|
|
57
|
+
## Modes
|
|
58
|
+
|
|
59
|
+
### Local Mode (default)
|
|
60
|
+
|
|
61
|
+
Reads and writes GEP assets from local files. Use when you have a local evolver installation (Cursor, VS Code, etc.).
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"gep": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@evomap/gep-mcp-server"],
|
|
69
|
+
"env": {
|
|
70
|
+
"GEP_ASSETS_DIR": "/path/to/your/gep/assets",
|
|
71
|
+
"GEP_MEMORY_DIR": "/path/to/your/memory/evolution"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Remote Mode
|
|
79
|
+
|
|
80
|
+
Delegates all memory operations to the EvoMap Hub API. Use for cloud agents (OpenClaw, Manus, etc.) that don't have local file access. Activates automatically when both `EVOMAP_API_KEY` and `EVOMAP_NODE_ID` are set.
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"gep": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "@evomap/gep-mcp-server"],
|
|
88
|
+
"env": {
|
|
89
|
+
"EVOMAP_API_KEY": "your-node-secret",
|
|
90
|
+
"EVOMAP_NODE_ID": "your-node-id",
|
|
91
|
+
"EVOMAP_HUB_URL": "https://evomap.ai"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
In remote mode:
|
|
99
|
+
- `gep_recall` calls `POST /a2a/memory/recall`
|
|
100
|
+
- `gep_record_outcome` calls `POST /a2a/memory/record`
|
|
101
|
+
- `gep_status` calls `GET /a2a/memory/status`
|
|
102
|
+
- `gep_evolve` combines recall + community search
|
|
103
|
+
- `gep_install_gene` and `gep_export` are unavailable (local-only)
|
|
104
|
+
|
|
105
|
+
## Environment Variables
|
|
106
|
+
|
|
107
|
+
| Variable | Default | Description |
|
|
108
|
+
|----------|---------|-------------|
|
|
109
|
+
| `GEP_ASSETS_DIR` | `./assets/gep` | (Local mode) Directory for gene pool, capsules, and event log |
|
|
110
|
+
| `GEP_MEMORY_DIR` | `./memory/evolution` | (Local mode) Directory for the memory graph |
|
|
111
|
+
| `EVOMAP_API_KEY` | -- | (Remote mode) Node secret from `/a2a/hello` |
|
|
112
|
+
| `EVOMAP_NODE_ID` | -- | (Remote mode) Your agent's node_id |
|
|
113
|
+
| `EVOMAP_HUB_URL` | `https://evomap.ai` | EvoMap Hub URL |
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Node.js >= 18.0.0
|
|
118
|
+
|
|
119
|
+
## Related
|
|
120
|
+
|
|
121
|
+
- [@evomap/gep-sdk](https://github.com/EvoMap/gep-sdk-js) -- GEP protocol SDK
|
|
122
|
+
- [@evomap/evolver](https://github.com/EvoMap/evolver) -- Full self-evolution engine
|
|
123
|
+
- [EvoMap](https://evomap.ai) -- Agent evolution network
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -15,15 +15,19 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
import { GepRuntime } from './runtime.js';
|
|
18
|
+
import { RemoteRuntime } from './remote.js';
|
|
18
19
|
|
|
19
20
|
const ASSETS_DIR = process.env.GEP_ASSETS_DIR || resolve(process.cwd(), 'assets/gep');
|
|
20
21
|
const MEMORY_DIR = process.env.GEP_MEMORY_DIR || resolve(process.cwd(), 'memory/evolution');
|
|
21
22
|
const HUB_URL = process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const IS_REMOTE = !!(process.env.EVOMAP_API_KEY && process.env.EVOMAP_NODE_ID);
|
|
25
|
+
const runtime = IS_REMOTE
|
|
26
|
+
? new RemoteRuntime({ hubUrl: HUB_URL, nodeId: process.env.EVOMAP_NODE_ID, apiKey: process.env.EVOMAP_API_KEY })
|
|
27
|
+
: new GepRuntime({ assetsDir: ASSETS_DIR, memoryDir: MEMORY_DIR });
|
|
24
28
|
|
|
25
29
|
const server = new Server(
|
|
26
|
-
{ name: 'gep-mcp-server', version: '1.0
|
|
30
|
+
{ name: 'gep-mcp-server', version: '1.2.0' },
|
|
27
31
|
{ capabilities: { tools: {}, resources: {} } }
|
|
28
32
|
);
|
|
29
33
|
|
|
@@ -63,19 +67,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
63
67
|
items: { type: 'string' },
|
|
64
68
|
description: 'Optional: specific signal patterns to search for',
|
|
65
69
|
},
|
|
70
|
+
limit: {
|
|
71
|
+
type: 'number',
|
|
72
|
+
description: 'Max results to return (default 10, max 50)',
|
|
73
|
+
},
|
|
66
74
|
},
|
|
67
75
|
required: ['query'],
|
|
68
76
|
},
|
|
69
77
|
},
|
|
70
78
|
{
|
|
71
79
|
name: 'gep_record_outcome',
|
|
72
|
-
description: 'Record the outcome of
|
|
80
|
+
description: 'Record the outcome of a task. Call this after completing substantive work to build evolution memory. The summary must describe both the problem and the solution.',
|
|
73
81
|
inputSchema: {
|
|
74
82
|
type: 'object',
|
|
75
83
|
properties: {
|
|
76
84
|
geneId: {
|
|
77
85
|
type: 'string',
|
|
78
|
-
description: 'The gene ID that was used',
|
|
86
|
+
description: 'The gene ID that was used (use "ad_hoc" for non-gene-driven tasks)',
|
|
79
87
|
},
|
|
80
88
|
signals: {
|
|
81
89
|
type: 'array',
|
|
@@ -85,7 +93,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
85
93
|
status: {
|
|
86
94
|
type: 'string',
|
|
87
95
|
enum: ['success', 'failed'],
|
|
88
|
-
description: 'Whether the
|
|
96
|
+
description: 'Whether the task was successful',
|
|
89
97
|
},
|
|
90
98
|
score: {
|
|
91
99
|
type: 'number',
|
|
@@ -93,10 +101,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
93
101
|
},
|
|
94
102
|
summary: {
|
|
95
103
|
type: 'string',
|
|
96
|
-
description: '
|
|
104
|
+
description: 'Specific description of what happened: "Fixed X by doing Y" (required for useful recall)',
|
|
97
105
|
},
|
|
98
106
|
},
|
|
99
|
-
required: ['geneId', 'signals', 'status', 'score'],
|
|
107
|
+
required: ['geneId', 'signals', 'status', 'score', 'summary'],
|
|
100
108
|
},
|
|
101
109
|
},
|
|
102
110
|
{
|
|
@@ -187,23 +195,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
187
195
|
try {
|
|
188
196
|
switch (name) {
|
|
189
197
|
case 'gep_evolve':
|
|
190
|
-
return { content: [{ type: 'text', text: JSON.stringify(runtime.evolve(args), null, 2) }] };
|
|
198
|
+
return { content: [{ type: 'text', text: JSON.stringify(await runtime.evolve(args), null, 2) }] };
|
|
191
199
|
case 'gep_recall':
|
|
192
|
-
return { content: [{ type: 'text', text: JSON.stringify(runtime.recall(args), null, 2) }] };
|
|
200
|
+
return { content: [{ type: 'text', text: JSON.stringify(await runtime.recall(args), null, 2) }] };
|
|
193
201
|
case 'gep_record_outcome':
|
|
194
|
-
return { content: [{ type: 'text', text: JSON.stringify(runtime.recordOutcome(args), null, 2) }] };
|
|
202
|
+
return { content: [{ type: 'text', text: JSON.stringify(await runtime.recordOutcome(args), null, 2) }] };
|
|
195
203
|
case 'gep_list_genes':
|
|
196
|
-
return { content: [{ type: 'text', text: JSON.stringify(runtime.listGenes(args), null, 2) }] };
|
|
197
|
-
case 'gep_install_gene':
|
|
204
|
+
return { content: [{ type: 'text', text: JSON.stringify(await runtime.listGenes(args), null, 2) }] };
|
|
205
|
+
case 'gep_install_gene': {
|
|
206
|
+
if (IS_REMOTE) return { content: [{ type: 'text', text: JSON.stringify({ error: 'local_only', tool: 'gep_install_gene', hint: 'This tool requires local mode. Set ASSETS_DIR and MEMORY_DIR instead of EVOMAP_API_KEY.' }) }], isError: true };
|
|
198
207
|
return { content: [{ type: 'text', text: JSON.stringify(runtime.installGene(args), null, 2) }] };
|
|
199
|
-
|
|
208
|
+
}
|
|
209
|
+
case 'gep_export': {
|
|
210
|
+
if (IS_REMOTE) return { content: [{ type: 'text', text: JSON.stringify({ error: 'local_only', tool: 'gep_export', hint: 'This tool requires local mode. Set ASSETS_DIR and MEMORY_DIR instead of EVOMAP_API_KEY.' }) }], isError: true };
|
|
200
211
|
return { content: [{ type: 'text', text: JSON.stringify(runtime.exportEvolution(args), null, 2) }] };
|
|
212
|
+
}
|
|
201
213
|
case 'gep_status':
|
|
202
|
-
return { content: [{ type: 'text', text: JSON.stringify(runtime.getStatus(), null, 2) }] };
|
|
214
|
+
return { content: [{ type: 'text', text: JSON.stringify(await runtime.getStatus(), null, 2) }] };
|
|
203
215
|
case 'gep_search_community': {
|
|
204
216
|
if (!args.query || typeof args.query !== 'string' || args.query.trim().length < 2) {
|
|
205
217
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'query must be a string with at least 2 characters' }) }], isError: true };
|
|
206
218
|
}
|
|
219
|
+
if (IS_REMOTE) {
|
|
220
|
+
const data = await runtime.searchCommunity(args);
|
|
221
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
222
|
+
}
|
|
207
223
|
const params = new URLSearchParams();
|
|
208
224
|
params.set('q', args.query.trim().slice(0, 500));
|
|
209
225
|
if (args.type && ['Gene', 'Capsule'].includes(args.type)) params.set('type', args.type);
|
|
@@ -261,10 +277,19 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
261
277
|
: 'GEP spec not found. Place gep-spec-v1.md in your GEP_ASSETS_DIR or install gep-protocol alongside this package.';
|
|
262
278
|
return { contents: [{ uri, mimeType: 'text/markdown', text: content }] };
|
|
263
279
|
}
|
|
264
|
-
case 'gep://genes':
|
|
280
|
+
case 'gep://genes': {
|
|
281
|
+
if (IS_REMOTE) {
|
|
282
|
+
const result = await runtime.listGenes({});
|
|
283
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(result.genes || [], null, 2) }] };
|
|
284
|
+
}
|
|
265
285
|
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(runtime.store.loadGenes(), null, 2) }] };
|
|
266
|
-
|
|
286
|
+
}
|
|
287
|
+
case 'gep://capsules': {
|
|
288
|
+
if (IS_REMOTE) {
|
|
289
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify({ note: 'Capsules are stored on EvoMap Hub. Use gep_recall to query evolution history.' }) }] };
|
|
290
|
+
}
|
|
267
291
|
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(runtime.store.loadCapsules(), null, 2) }] };
|
|
292
|
+
}
|
|
268
293
|
default:
|
|
269
294
|
throw new Error(`Unknown resource: ${uri}`);
|
|
270
295
|
}
|
|
@@ -273,7 +298,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
273
298
|
async function main() {
|
|
274
299
|
const transport = new StdioServerTransport();
|
|
275
300
|
await server.connect(transport);
|
|
276
|
-
console.error(
|
|
301
|
+
console.error(`GEP MCP Server running on stdio (${IS_REMOTE ? 'remote' : 'local'} mode)`);
|
|
277
302
|
}
|
|
278
303
|
|
|
279
304
|
main().catch(err => {
|
package/src/remote.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const DEFAULT_HUB_URL = 'https://evomap.ai';
|
|
2
|
+
const TIMEOUT_MS = 15000;
|
|
3
|
+
|
|
4
|
+
export class RemoteRuntime {
|
|
5
|
+
constructor({ hubUrl, nodeId, apiKey }) {
|
|
6
|
+
this.hubUrl = (hubUrl || DEFAULT_HUB_URL).replace(/\/+$/, '');
|
|
7
|
+
this.nodeId = nodeId;
|
|
8
|
+
this.apiKey = apiKey;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async _request(method, path, body) {
|
|
12
|
+
const url = `${this.hubUrl}${path}`;
|
|
13
|
+
const headers = {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
16
|
+
};
|
|
17
|
+
const opts = { method, headers, signal: AbortSignal.timeout(TIMEOUT_MS) };
|
|
18
|
+
if (body) opts.body = JSON.stringify(body);
|
|
19
|
+
const res = await fetch(url, opts);
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const text = await res.text().catch(() => '');
|
|
22
|
+
throw new Error(`Hub ${method} ${path} returned ${res.status}: ${text.slice(0, 200)}`);
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async recall(args) {
|
|
28
|
+
const { query, signals, limit } = args || {};
|
|
29
|
+
const effectiveLimit = Math.min(Math.max(1, parseInt(limit, 10) || 10), 50);
|
|
30
|
+
return this._request('POST', '/a2a/memory/recall', {
|
|
31
|
+
node_id: this.nodeId,
|
|
32
|
+
query,
|
|
33
|
+
signals,
|
|
34
|
+
limit: effectiveLimit,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async recordOutcome(args) {
|
|
39
|
+
const { geneId, signals, status, score, summary } = args || {};
|
|
40
|
+
return this._request('POST', '/a2a/memory/record', {
|
|
41
|
+
node_id: this.nodeId,
|
|
42
|
+
signals,
|
|
43
|
+
gene_id: geneId,
|
|
44
|
+
status,
|
|
45
|
+
score,
|
|
46
|
+
summary,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getStatus() {
|
|
51
|
+
return this._request('GET', `/a2a/memory/status?node_id=${encodeURIComponent(this.nodeId)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async evolve(args) {
|
|
55
|
+
const { context, intent } = args || {};
|
|
56
|
+
const intentSignals = intent ? [`intent:${intent}`] : [];
|
|
57
|
+
const recallResult = await this.recall({ query: context, signals: intentSignals.length ? intentSignals : null });
|
|
58
|
+
const signals = recallResult.signals_extracted || [];
|
|
59
|
+
|
|
60
|
+
let communityGenes = [];
|
|
61
|
+
try {
|
|
62
|
+
const searchQuery = intent
|
|
63
|
+
? `${intent} ${(context || '').slice(0, 150)}`
|
|
64
|
+
: (context || '').slice(0, 200);
|
|
65
|
+
const params = new URLSearchParams({ q: searchQuery, type: 'Gene', limit: '5' });
|
|
66
|
+
const searchResult = await this._request('GET', `/a2a/assets/semantic-search?${params}`);
|
|
67
|
+
communityGenes = (searchResult.assets || []).slice(0, 3);
|
|
68
|
+
} catch { /* best effort */ }
|
|
69
|
+
|
|
70
|
+
const matches = recallResult.matches || [];
|
|
71
|
+
const bestMatch = matches.length > 0 ? matches[0] : null;
|
|
72
|
+
|
|
73
|
+
const actionableAdvice = [];
|
|
74
|
+
if (bestMatch) {
|
|
75
|
+
const status = bestMatch.outcome?.status || bestMatch.status;
|
|
76
|
+
const summary = bestMatch.outcome?.summary || bestMatch.summary || '';
|
|
77
|
+
if (status === 'success') {
|
|
78
|
+
actionableAdvice.push(`Prior success (score ${bestMatch.score ?? 'N/A'}): ${summary}`);
|
|
79
|
+
actionableAdvice.push('Follow the same approach unless context has changed.');
|
|
80
|
+
} else if (status === 'failed') {
|
|
81
|
+
actionableAdvice.push(`Prior failure: ${summary}`);
|
|
82
|
+
actionableAdvice.push('Avoid repeating this approach -- try a different strategy.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const geneStrategies = communityGenes
|
|
87
|
+
.filter(g => g.strategy || g.strategy_steps)
|
|
88
|
+
.map(g => ({
|
|
89
|
+
id: g.asset_id || g.id,
|
|
90
|
+
category: g.category,
|
|
91
|
+
summary: g.summary || g.description,
|
|
92
|
+
strategy: g.strategy || g.strategy_steps,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
mode: 'remote',
|
|
98
|
+
intent: intent || null,
|
|
99
|
+
signals,
|
|
100
|
+
recall_matches: matches,
|
|
101
|
+
best_match_advice: actionableAdvice.length > 0 ? actionableAdvice : null,
|
|
102
|
+
community_genes: communityGenes.map(g => ({
|
|
103
|
+
id: g.asset_id || g.id,
|
|
104
|
+
category: g.category,
|
|
105
|
+
summary: g.summary || g.description,
|
|
106
|
+
})),
|
|
107
|
+
gene_strategies: geneStrategies.length > 0 ? geneStrategies : null,
|
|
108
|
+
instructions: [
|
|
109
|
+
...(actionableAdvice.length > 0
|
|
110
|
+
? ['Apply the advice from prior experience above.']
|
|
111
|
+
: ['No prior experience found. Proceed with best judgment.']),
|
|
112
|
+
...(geneStrategies.length > 0
|
|
113
|
+
? ['Community gene strategies are available -- review and apply if relevant.']
|
|
114
|
+
: []),
|
|
115
|
+
'After completing the task, call gep_record_outcome to record the result.',
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async listGenes(args) {
|
|
121
|
+
try {
|
|
122
|
+
const params = new URLSearchParams({ q: args?.category || 'evolution strategy', type: 'Gene', limit: '20' });
|
|
123
|
+
const result = await this._request('GET', `/a2a/assets/semantic-search?${params}`);
|
|
124
|
+
return {
|
|
125
|
+
total: (result.assets || []).length,
|
|
126
|
+
source: 'hub',
|
|
127
|
+
genes: (result.assets || []).map(g => ({
|
|
128
|
+
id: g.asset_id || g.id,
|
|
129
|
+
category: g.category,
|
|
130
|
+
summary: g.summary || g.description,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return { total: 0, source: 'hub', genes: [], error: err.message };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async searchCommunity(args) {
|
|
139
|
+
const params = new URLSearchParams();
|
|
140
|
+
params.set('q', (args?.query || '').slice(0, 500));
|
|
141
|
+
if (args?.type) params.set('type', args.type);
|
|
142
|
+
if (args?.outcome) params.set('outcome', args.outcome);
|
|
143
|
+
params.set('limit', String(Math.min(Math.max(1, parseInt(args?.limit, 10) || 10), 50)));
|
|
144
|
+
params.set('include_context', 'true');
|
|
145
|
+
return this._request('GET', `/a2a/assets/semantic-search?${params}`);
|
|
146
|
+
}
|
|
147
|
+
}
|