agr-mcp-server 4.0.3 → 5.0.2

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/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EntityType, SearchResponse, GeneData, DiseaseResponse, ExpressionResponse, OrthologyResponse, PhenotypeResponse, InteractionResponse, AlleleResponse, Species } from "./types.js";
1
+ import { EntityType, SearchResponse, GeneData, DiseaseResponse, ExpressionResponse, OrthologyResponse, PhenotypeResponse, InteractionResponse, AlleleResponse, Species, QueryBuilder, PathQueryResult, MineTemplate, MineList, ListContents } from "./types.js";
2
2
  export declare class AllianceClient {
3
3
  private agrApiUrl;
4
4
  private allianceMineUrl;
@@ -9,9 +9,14 @@ export declare class AllianceClient {
9
9
  */
10
10
  search(query: string, category?: EntityType, species?: string, limit?: number): Promise<SearchResponse>;
11
11
  /**
12
- * Search using AllianceMine for more advanced queries
12
+ * Search using AllianceMine for more advanced queries.
13
+ * Falls back to PathQuery-based search if keyword search fails.
13
14
  */
14
15
  searchMine(query: string, type?: string, limit?: number): Promise<SearchResponse>;
16
+ /**
17
+ * Fallback search using PathQuery when keyword search is unavailable
18
+ */
19
+ private searchMineViaPathQuery;
15
20
  private parseMineResults;
16
21
  /**
17
22
  * Get detailed gene information
@@ -57,4 +62,55 @@ export declare class AllianceClient {
57
62
  * Helper to resolve species short name
58
63
  */
59
64
  resolveSpecies(input: string): Species | null;
65
+ private getAuthToken;
66
+ private fetchWithAuth;
67
+ /**
68
+ * Run a raw PathQuery XML query against AllianceMine
69
+ */
70
+ runPathQuery(xml: string): Promise<PathQueryResult>;
71
+ /**
72
+ * Build PathQuery XML from QueryBuilder definition
73
+ */
74
+ buildPathQueryXml(query: QueryBuilder): string;
75
+ private escapeXml;
76
+ /**
77
+ * Build and run a query using the QueryBuilder DSL
78
+ */
79
+ buildAndRunQuery(query: QueryBuilder): Promise<PathQueryResult>;
80
+ /**
81
+ * List available query templates
82
+ */
83
+ listTemplates(): Promise<MineTemplate[]>;
84
+ /**
85
+ * Run a template query with parameters.
86
+ * Parameters should be provided as { "1": { path: "Gene", op: "LOOKUP", value: "BRCA1" } }
87
+ * or simplified as { "1": "BRCA1" } for LOOKUP constraints (path and op inferred from template).
88
+ */
89
+ runTemplate(name: string, params: Record<string, string | {
90
+ path?: string;
91
+ op?: string;
92
+ value: string;
93
+ }>, limit?: number): Promise<PathQueryResult>;
94
+ /**
95
+ * Get all available lists
96
+ */
97
+ getLists(type?: string): Promise<MineList[]>;
98
+ /**
99
+ * Get contents of a specific list
100
+ */
101
+ getList(name: string): Promise<ListContents | null>;
102
+ /**
103
+ * Create a new list (requires authentication)
104
+ */
105
+ createList(name: string, type: string, identifiers: string[], description?: string): Promise<MineList>;
106
+ /**
107
+ * Add items to an existing list (requires authentication)
108
+ */
109
+ addToList(name: string, identifiers: string[]): Promise<MineList>;
110
+ /**
111
+ * Delete a list (requires authentication)
112
+ */
113
+ deleteList(name: string): Promise<{
114
+ success: boolean;
115
+ }>;
60
116
  }
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SPECIES_MAP, SPECIES, } from "./types.js";
2
2
  const AGR_API_URL = "https://www.alliancegenome.org/api";
3
- const ALLIANCEMINE_URL = "https://www.alliancegenome.org/alliancemine/service";
3
+ const ALLIANCEMINE_URL = "https://alliancemine.alliancegenome.org/alliancemine/service";
4
4
  export class AllianceClient {
5
5
  agrApiUrl;
6
6
  allianceMineUrl;
@@ -64,9 +64,11 @@ export class AllianceClient {
64
64
  }
65
65
  }
66
66
  /**
67
- * Search using AllianceMine for more advanced queries
67
+ * Search using AllianceMine for more advanced queries.
68
+ * Falls back to PathQuery-based search if keyword search fails.
68
69
  */
69
70
  async searchMine(query, type, limit = 20) {
71
+ // Try keyword search first
70
72
  const params = new URLSearchParams({
71
73
  q: query,
72
74
  size: String(limit),
@@ -77,13 +79,51 @@ export class AllianceClient {
77
79
  const url = `${this.allianceMineUrl}/search?${params.toString()}`;
78
80
  try {
79
81
  const response = await this.fetch(url);
82
+ // Check if search service returned an error
83
+ if (response.wasSuccessful === false || response.error) {
84
+ throw new Error(response.error || "Search service unavailable");
85
+ }
80
86
  return {
81
87
  query,
82
88
  results: this.parseMineResults(response.results, limit),
83
89
  total: response.totalHits,
84
90
  };
85
91
  }
86
- catch (error) {
92
+ catch {
93
+ // Fallback to PathQuery-based search
94
+ return this.searchMineViaPathQuery(query, type, limit);
95
+ }
96
+ }
97
+ /**
98
+ * Fallback search using PathQuery when keyword search is unavailable
99
+ */
100
+ async searchMineViaPathQuery(query, type, limit = 20) {
101
+ const searchType = type || "Gene";
102
+ const xml = `<query model="genomic" view="${searchType}.primaryIdentifier ${searchType}.symbol ${searchType}.name ${searchType}.organism.shortName">
103
+ <constraint path="${searchType}.symbol" op="CONTAINS" value="${this.escapeXml(query)}"/>
104
+ </query>`;
105
+ const params = new URLSearchParams({
106
+ query: xml,
107
+ format: "json",
108
+ size: String(limit),
109
+ });
110
+ const url = `${this.allianceMineUrl}/query/results?${params.toString()}`;
111
+ try {
112
+ const response = await this.fetch(url);
113
+ const results = response.results.map((row) => ({
114
+ id: String(row[0] || ""),
115
+ symbol: String(row[1] || ""),
116
+ name: String(row[2] || row[1] || ""),
117
+ species: String(row[3] || ""),
118
+ category: searchType.toLowerCase(),
119
+ }));
120
+ return {
121
+ query,
122
+ results,
123
+ total: results.length,
124
+ };
125
+ }
126
+ catch {
87
127
  return { query, results: [], total: 0 };
88
128
  }
89
129
  }
@@ -238,4 +278,297 @@ export class AllianceClient {
238
278
  }
239
279
  return null;
240
280
  }
281
+ // ============================================
282
+ // AllianceMine API Methods
283
+ // ============================================
284
+ getAuthToken() {
285
+ return process.env.ALLIANCEMINE_TOKEN || null;
286
+ }
287
+ async fetchWithAuth(url, options = {}) {
288
+ const token = this.getAuthToken();
289
+ if (!token) {
290
+ throw new Error("Authentication required. Set ALLIANCEMINE_TOKEN environment variable.");
291
+ }
292
+ const headers = {
293
+ Accept: "application/json",
294
+ "Content-Type": "application/json",
295
+ Authorization: `Token ${token}`,
296
+ };
297
+ const response = await fetch(url, {
298
+ ...options,
299
+ headers: { ...headers, ...options.headers },
300
+ });
301
+ if (!response.ok) {
302
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
303
+ }
304
+ return response.json();
305
+ }
306
+ /**
307
+ * Run a raw PathQuery XML query against AllianceMine
308
+ */
309
+ async runPathQuery(xml) {
310
+ const params = new URLSearchParams({
311
+ query: xml,
312
+ format: "json",
313
+ });
314
+ const url = `${this.allianceMineUrl}/query/results?${params.toString()}`;
315
+ try {
316
+ const response = await this.fetch(url);
317
+ // Convert array results to objects using column headers
318
+ const headers = response.columnHeaders || [];
319
+ const results = response.results.map((row) => {
320
+ const obj = {};
321
+ row.forEach((val, idx) => {
322
+ const key = headers[idx] || `col${idx}`;
323
+ obj[key] = val;
324
+ });
325
+ return obj;
326
+ });
327
+ return {
328
+ results,
329
+ columnHeaders: headers,
330
+ rootClass: response.rootClass,
331
+ };
332
+ }
333
+ catch (error) {
334
+ throw new Error(`PathQuery failed: ${error}`);
335
+ }
336
+ }
337
+ /**
338
+ * Build PathQuery XML from QueryBuilder definition
339
+ */
340
+ buildPathQueryXml(query) {
341
+ const { from, select, where, joins, sort } = query;
342
+ // Prefix select fields with root class if not already prefixed
343
+ const viewFields = select.map((field) => field.startsWith(`${from}.`) ? field : `${from}.${field}`);
344
+ let xml = `<query model="genomic" view="${viewFields.join(" ")}"`;
345
+ if (sort) {
346
+ xml += ` sortOrder="${sort.field} ${sort.direction}"`;
347
+ }
348
+ xml += ">";
349
+ // Add outer joins
350
+ if (joins && joins.length > 0) {
351
+ for (const join of joins) {
352
+ xml += `<join path="${from}.${join}" style="OUTER"/>`;
353
+ }
354
+ }
355
+ // Add constraints
356
+ if (where) {
357
+ let code = "A";
358
+ for (const [field, constraint] of Object.entries(where)) {
359
+ const path = field.includes(".") ? `${from}.${field}` : `${from}.${field}`;
360
+ if (typeof constraint === "string") {
361
+ xml += `<constraint path="${path}" op="=" value="${this.escapeXml(constraint)}" code="${code}"/>`;
362
+ }
363
+ else {
364
+ const c = constraint;
365
+ if (Array.isArray(c.value)) {
366
+ xml += `<constraint path="${path}" op="${c.op}" code="${code}">${c.value.map((v) => `<value>${this.escapeXml(v)}</value>`).join("")}</constraint>`;
367
+ }
368
+ else if (c.op === "IS NULL" || c.op === "IS NOT NULL") {
369
+ xml += `<constraint path="${path}" op="${c.op}" code="${code}"/>`;
370
+ }
371
+ else {
372
+ xml += `<constraint path="${path}" op="${c.op}" value="${this.escapeXml(String(c.value))}" code="${code}"/>`;
373
+ }
374
+ }
375
+ code = String.fromCharCode(code.charCodeAt(0) + 1);
376
+ }
377
+ }
378
+ xml += "</query>";
379
+ return xml;
380
+ }
381
+ escapeXml(str) {
382
+ return str
383
+ .replace(/&/g, "&amp;")
384
+ .replace(/</g, "&lt;")
385
+ .replace(/>/g, "&gt;")
386
+ .replace(/"/g, "&quot;")
387
+ .replace(/'/g, "&apos;");
388
+ }
389
+ /**
390
+ * Build and run a query using the QueryBuilder DSL
391
+ */
392
+ async buildAndRunQuery(query) {
393
+ const xml = this.buildPathQueryXml(query);
394
+ // Add limit via size parameter
395
+ const params = new URLSearchParams({
396
+ query: xml,
397
+ format: "json",
398
+ size: String(query.limit || 100),
399
+ });
400
+ const url = `${this.allianceMineUrl}/query/results?${params.toString()}`;
401
+ try {
402
+ const response = await this.fetch(url);
403
+ const headers = response.columnHeaders || [];
404
+ const results = response.results.map((row) => {
405
+ const obj = {};
406
+ row.forEach((val, idx) => {
407
+ const key = headers[idx] || `col${idx}`;
408
+ obj[key] = val;
409
+ });
410
+ return obj;
411
+ });
412
+ return {
413
+ results,
414
+ columnHeaders: headers,
415
+ rootClass: response.rootClass,
416
+ };
417
+ }
418
+ catch (error) {
419
+ throw new Error(`Query failed: ${error}`);
420
+ }
421
+ }
422
+ /**
423
+ * List available query templates
424
+ */
425
+ async listTemplates() {
426
+ const url = `${this.allianceMineUrl}/templates?format=json`;
427
+ try {
428
+ const response = await this.fetch(url);
429
+ return Object.entries(response.templates).map(([name, template]) => ({
430
+ ...template,
431
+ name,
432
+ }));
433
+ }
434
+ catch (error) {
435
+ return [];
436
+ }
437
+ }
438
+ /**
439
+ * Run a template query with parameters.
440
+ * Parameters should be provided as { "1": { path: "Gene", op: "LOOKUP", value: "BRCA1" } }
441
+ * or simplified as { "1": "BRCA1" } for LOOKUP constraints (path and op inferred from template).
442
+ */
443
+ async runTemplate(name, params, limit = 100) {
444
+ const queryParams = new URLSearchParams({
445
+ name,
446
+ format: "json",
447
+ size: String(limit),
448
+ });
449
+ // Add template parameters
450
+ for (const [key, param] of Object.entries(params)) {
451
+ if (typeof param === "string") {
452
+ // Simple format: just the value, assume LOOKUP
453
+ queryParams.set(`value${key}`, param);
454
+ }
455
+ else {
456
+ // Full format with path, op, value
457
+ if (param.path)
458
+ queryParams.set(`constraint${key}`, param.path);
459
+ if (param.op)
460
+ queryParams.set(`op${key}`, param.op);
461
+ queryParams.set(`value${key}`, param.value);
462
+ }
463
+ }
464
+ const url = `${this.allianceMineUrl}/template/results?${queryParams.toString()}`;
465
+ try {
466
+ const response = await this.fetch(url);
467
+ const headers = response.columnHeaders || [];
468
+ const results = response.results.map((row) => {
469
+ const obj = {};
470
+ row.forEach((val, idx) => {
471
+ const key = headers[idx] || `col${idx}`;
472
+ obj[key] = val;
473
+ });
474
+ return obj;
475
+ });
476
+ return {
477
+ results,
478
+ columnHeaders: headers,
479
+ };
480
+ }
481
+ catch (error) {
482
+ throw new Error(`Template query failed: ${error}`);
483
+ }
484
+ }
485
+ /**
486
+ * Get all available lists
487
+ */
488
+ async getLists(type) {
489
+ const params = new URLSearchParams();
490
+ if (type) {
491
+ params.set("type", type);
492
+ }
493
+ const url = `${this.allianceMineUrl}/lists?${params.toString()}`;
494
+ try {
495
+ const response = await this.fetch(url);
496
+ return response.lists || [];
497
+ }
498
+ catch (error) {
499
+ return [];
500
+ }
501
+ }
502
+ /**
503
+ * Get contents of a specific list
504
+ */
505
+ async getList(name) {
506
+ const url = `${this.allianceMineUrl}/list/results/${encodeURIComponent(name)}?format=json`;
507
+ try {
508
+ const response = await this.fetch(url);
509
+ const headers = response.columnHeaders || [];
510
+ const results = response.results.map((row) => {
511
+ const obj = {};
512
+ row.forEach((val, idx) => {
513
+ const key = headers[idx] || `col${idx}`;
514
+ obj[key] = val;
515
+ });
516
+ return obj;
517
+ });
518
+ return {
519
+ name: response.listInfo?.name || name,
520
+ type: response.listInfo?.type || "unknown",
521
+ size: response.listInfo?.size || results.length,
522
+ results,
523
+ };
524
+ }
525
+ catch (error) {
526
+ return null;
527
+ }
528
+ }
529
+ /**
530
+ * Create a new list (requires authentication)
531
+ */
532
+ async createList(name, type, identifiers, description) {
533
+ const params = new URLSearchParams({
534
+ name,
535
+ type,
536
+ });
537
+ if (description) {
538
+ params.set("description", description);
539
+ }
540
+ const url = `${this.allianceMineUrl}/lists?${params.toString()}`;
541
+ const response = await this.fetchWithAuth(url, {
542
+ method: "POST",
543
+ body: identifiers.join("\n"),
544
+ headers: {
545
+ "Content-Type": "text/plain",
546
+ },
547
+ });
548
+ return response;
549
+ }
550
+ /**
551
+ * Add items to an existing list (requires authentication)
552
+ */
553
+ async addToList(name, identifiers) {
554
+ const url = `${this.allianceMineUrl}/lists/append/${encodeURIComponent(name)}`;
555
+ const response = await this.fetchWithAuth(url, {
556
+ method: "POST",
557
+ body: identifiers.join("\n"),
558
+ headers: {
559
+ "Content-Type": "text/plain",
560
+ },
561
+ });
562
+ return response;
563
+ }
564
+ /**
565
+ * Delete a list (requires authentication)
566
+ */
567
+ async deleteList(name) {
568
+ const url = `${this.allianceMineUrl}/lists/${encodeURIComponent(name)}`;
569
+ await this.fetchWithAuth(url, {
570
+ method: "DELETE",
571
+ });
572
+ return { success: true };
573
+ }
241
574
  }
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { ENTITY_TYPES } from "./types.js";
7
7
  const client = new AllianceClient();
8
8
  const server = new McpServer({
9
9
  name: "agr-genomics",
10
- version: "4.0.3",
10
+ version: "5.0.2",
11
11
  });
12
12
  // Tool: Search genes
13
13
  server.tool("search_genes", "Search for genes across all Alliance of Genome Resources model organisms. Supports species filtering.", {
@@ -234,6 +234,444 @@ server.tool("get_species_list", "Get list of model organisms supported by Allian
234
234
  };
235
235
  }
236
236
  });
237
+ // ============================================
238
+ // AllianceMine Tools
239
+ // ============================================
240
+ // Tool: Mine Search
241
+ server.tool("mine_search", "Search AllianceMine for genes, proteins, diseases, and other biological entities using keyword search.", {
242
+ query: z.string().describe("Search query - keyword, gene symbol, protein name, etc."),
243
+ type: z
244
+ .string()
245
+ .optional()
246
+ .describe("Filter by type: Gene, Protein, Disease, Pathway, etc."),
247
+ limit: z.number().optional().default(20).describe("Maximum results"),
248
+ }, async ({ query, type, limit }) => {
249
+ try {
250
+ const results = await client.searchMine(query, type, limit);
251
+ return {
252
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
253
+ };
254
+ }
255
+ catch (error) {
256
+ return {
257
+ content: [{ type: "text", text: `Error searching AllianceMine: ${error}` }],
258
+ isError: true,
259
+ };
260
+ }
261
+ });
262
+ // Tool: Mine Query (raw PathQuery XML)
263
+ server.tool("mine_query", "Run a raw PathQuery XML query against AllianceMine. For power users who know InterMine PathQuery syntax.", {
264
+ xml: z.string().describe("PathQuery XML string"),
265
+ }, async ({ xml }) => {
266
+ try {
267
+ const results = await client.runPathQuery(xml);
268
+ return {
269
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
270
+ };
271
+ }
272
+ catch (error) {
273
+ return {
274
+ content: [{ type: "text", text: `PathQuery error: ${error}` }],
275
+ isError: true,
276
+ };
277
+ }
278
+ });
279
+ // Tool: Mine Query Builder
280
+ server.tool("mine_query_builder", `Build and run structured queries against AllianceMine using a JSON DSL.
281
+
282
+ Example query - find human genes in DNA repair pathway:
283
+ {
284
+ "from": "Gene",
285
+ "select": ["symbol", "name", "organism.name", "pathways.name"],
286
+ "where": {
287
+ "organism.name": "Homo sapiens",
288
+ "pathways.name": { "op": "CONTAINS", "value": "DNA repair" }
289
+ },
290
+ "limit": 50
291
+ }
292
+
293
+ Supported operators: =, !=, CONTAINS, LIKE, <, >, <=, >=, ONE OF, NONE OF, IS NULL, IS NOT NULL`, {
294
+ from: z.string().describe("Root class: Gene, Protein, Disease, Pathway, Phenotype, etc."),
295
+ select: z.array(z.string()).describe("Fields to return, e.g., ['symbol', 'organism.name']"),
296
+ where: z
297
+ .record(z.union([
298
+ z.string(),
299
+ z.object({
300
+ op: z.string(),
301
+ value: z.union([z.string(), z.array(z.string())]),
302
+ }),
303
+ ]))
304
+ .optional()
305
+ .describe("Constraints as field: value or field: {op, value}"),
306
+ joins: z.array(z.string()).optional().describe("OUTER JOIN paths for optional relationships"),
307
+ sort: z
308
+ .object({
309
+ field: z.string(),
310
+ direction: z.enum(["ASC", "DESC"]),
311
+ })
312
+ .optional()
313
+ .describe("Sort order"),
314
+ limit: z.number().optional().default(100).describe("Maximum results"),
315
+ }, async ({ from, select, where, joins, sort, limit }) => {
316
+ try {
317
+ const results = await client.buildAndRunQuery({
318
+ from,
319
+ select,
320
+ where: where,
321
+ joins,
322
+ sort,
323
+ limit,
324
+ });
325
+ return {
326
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
327
+ };
328
+ }
329
+ catch (error) {
330
+ return {
331
+ content: [{ type: "text", text: `Query error: ${error}` }],
332
+ isError: true,
333
+ };
334
+ }
335
+ });
336
+ // Tool: Mine Natural Query
337
+ server.tool("mine_natural_query", `Process a natural language query and return schema information to construct a structured AllianceMine query.
338
+
339
+ This tool returns the AllianceMine schema so you can convert the user's natural language into a mine_query_builder call.`, {
340
+ query: z.string().describe("Natural language query describing what to find"),
341
+ limit: z.number().optional().default(100).describe("Maximum results"),
342
+ }, async ({ query, limit }) => {
343
+ // Return comprehensive schema for LLM to construct the query
344
+ const schema = {
345
+ user_query: query,
346
+ requested_limit: limit,
347
+ instruction: "Use mine_query_builder to execute this query. Analyze the user's natural language and construct the appropriate structured query using the schema below.",
348
+ classes: {
349
+ Gene: {
350
+ description: "Genes from all model organisms",
351
+ common_fields: ["primaryIdentifier", "symbol", "name", "description", "secondaryIdentifier"],
352
+ relationships: {
353
+ "organism": "Organism (use organism.shortName or organism.name)",
354
+ "chromosome": "Chromosome (use chromosome.primaryIdentifier)",
355
+ "diseases": "Disease associations",
356
+ "phenotypes": "Phenotype annotations",
357
+ "pathways": "Pathway memberships",
358
+ "alleles": "Gene alleles/variants",
359
+ "goAnnotation": "GO term annotations",
360
+ "homologues": "Ortholog/paralog relationships",
361
+ "proteins": "Encoded proteins",
362
+ "publications": "Related publications"
363
+ }
364
+ },
365
+ Protein: {
366
+ description: "Protein records",
367
+ common_fields: ["primaryIdentifier", "primaryAccession", "name", "length"],
368
+ relationships: {
369
+ "genes": "Encoding genes",
370
+ "organism": "Source organism",
371
+ "proteinDomains": "Protein domains"
372
+ }
373
+ },
374
+ Disease: {
375
+ description: "Disease ontology terms (DO)",
376
+ common_fields: ["primaryIdentifier", "name", "description"],
377
+ relationships: {
378
+ "genes": "Associated genes",
379
+ "alleles": "Associated alleles",
380
+ "parents": "Parent terms",
381
+ "children": "Child terms"
382
+ }
383
+ },
384
+ Pathway: {
385
+ description: "Biological pathways",
386
+ common_fields: ["primaryIdentifier", "name", "description"],
387
+ relationships: {
388
+ "genes": "Genes in pathway",
389
+ "dataSets": "Data sources"
390
+ }
391
+ },
392
+ Phenotype: {
393
+ description: "Phenotype annotations",
394
+ common_fields: ["primaryIdentifier", "description"],
395
+ relationships: {
396
+ "genes": "Associated genes",
397
+ "alleles": "Associated alleles"
398
+ }
399
+ },
400
+ Allele: {
401
+ description: "Gene alleles and variants",
402
+ common_fields: ["primaryIdentifier", "symbol", "name", "alleleClass", "description"],
403
+ relationships: {
404
+ "gene": "Parent gene",
405
+ "organism": "Source organism",
406
+ "phenotypes": "Associated phenotypes"
407
+ }
408
+ },
409
+ GOTerm: {
410
+ description: "Gene Ontology terms",
411
+ common_fields: ["primaryIdentifier", "name", "description", "namespace"],
412
+ relationships: {
413
+ "genes": "Annotated genes",
414
+ "parents": "Parent terms",
415
+ "children": "Child terms"
416
+ }
417
+ },
418
+ Publication: {
419
+ description: "Scientific publications",
420
+ common_fields: ["primaryIdentifier", "pubMedId", "title", "firstAuthor", "year", "journal"],
421
+ relationships: {
422
+ "genes": "Mentioned genes",
423
+ "authors": "Author list"
424
+ }
425
+ },
426
+ Organism: {
427
+ description: "Species/organisms",
428
+ common_fields: ["shortName", "name", "taxonId"],
429
+ short_names: {
430
+ "H. sapiens": "Human",
431
+ "M. musculus": "Mouse",
432
+ "R. norvegicus": "Rat",
433
+ "D. rerio": "Zebrafish",
434
+ "D. melanogaster": "Fly",
435
+ "C. elegans": "Worm",
436
+ "S. cerevisiae": "Yeast",
437
+ "X. laevis": "Frog (Xenopus laevis)",
438
+ "X. tropicalis": "Frog (Xenopus tropicalis)"
439
+ }
440
+ }
441
+ },
442
+ operators: {
443
+ "=": "Exact match",
444
+ "!=": "Not equal",
445
+ "CONTAINS": "Contains substring (case-insensitive)",
446
+ "LIKE": "Pattern match with wildcards (*)",
447
+ "<": "Less than",
448
+ ">": "Greater than",
449
+ "<=": "Less than or equal",
450
+ ">=": "Greater than or equal",
451
+ "ONE OF": "Match any in list (value should be array)",
452
+ "NONE OF": "Match none in list (value should be array)",
453
+ "IS NULL": "Field is empty",
454
+ "IS NOT NULL": "Field has value"
455
+ },
456
+ query_builder_format: {
457
+ from: "Root class name (e.g., 'Gene')",
458
+ select: "Array of fields to return (e.g., ['primaryIdentifier', 'symbol', 'name'])",
459
+ where: "Object with field constraints (e.g., { 'symbol': { op: 'CONTAINS', value: 'BRCA' } })",
460
+ joins: "Optional: array of paths to OUTER JOIN (for optional relationships)",
461
+ sort: "Optional: { field: 'symbol', direction: 'ASC' }",
462
+ limit: "Optional: max results (default 100)"
463
+ },
464
+ examples: [
465
+ {
466
+ natural: "human genes related to cancer",
467
+ structured: {
468
+ from: "Gene",
469
+ select: ["primaryIdentifier", "symbol", "name", "organism.shortName"],
470
+ where: {
471
+ "organism.shortName": "H. sapiens",
472
+ "name": { op: "CONTAINS", value: "cancer" }
473
+ },
474
+ limit: 100
475
+ }
476
+ },
477
+ {
478
+ natural: "mouse genes on chromosome 11",
479
+ structured: {
480
+ from: "Gene",
481
+ select: ["primaryIdentifier", "symbol", "name", "chromosome.primaryIdentifier"],
482
+ where: {
483
+ "organism.shortName": "M. musculus",
484
+ "chromosome.primaryIdentifier": "11"
485
+ }
486
+ }
487
+ },
488
+ {
489
+ natural: "diseases associated with BRCA1",
490
+ structured: {
491
+ from: "Disease",
492
+ select: ["primaryIdentifier", "name", "genes.symbol"],
493
+ where: {
494
+ "genes.symbol": "BRCA1"
495
+ }
496
+ }
497
+ },
498
+ {
499
+ natural: "all yeast genes with kinase activity",
500
+ structured: {
501
+ from: "Gene",
502
+ select: ["primaryIdentifier", "symbol", "name", "goAnnotation.ontologyTerm.name"],
503
+ where: {
504
+ "organism.shortName": "S. cerevisiae",
505
+ "goAnnotation.ontologyTerm.name": { op: "CONTAINS", value: "kinase" }
506
+ }
507
+ }
508
+ }
509
+ ]
510
+ };
511
+ return {
512
+ content: [
513
+ {
514
+ type: "text",
515
+ text: JSON.stringify(schema, null, 2),
516
+ },
517
+ ],
518
+ };
519
+ });
520
+ // Tool: List Templates
521
+ server.tool("mine_list_templates", "List available query templates in AllianceMine. Templates are pre-built queries for common use cases.", {}, async () => {
522
+ try {
523
+ const templates = await client.listTemplates();
524
+ return {
525
+ content: [{ type: "text", text: JSON.stringify(templates, null, 2) }],
526
+ };
527
+ }
528
+ catch (error) {
529
+ return {
530
+ content: [{ type: "text", text: `Error listing templates: ${error}` }],
531
+ isError: true,
532
+ };
533
+ }
534
+ });
535
+ // Tool: Run Template
536
+ server.tool("mine_run_template", `Run a pre-built query template with parameters.
537
+
538
+ Parameter format: Use numeric keys ("1", "2", etc.) matching constraint positions.
539
+ - Simple: {"1": "BRCA1"} - just the value
540
+ - Full: {"1": {"path": "Gene", "op": "LOOKUP", "value": "BRCA1"}}
541
+
542
+ Common templates:
543
+ - Gene_Orthologs: Find orthologs (params: {"1": {"path": "Gene", "op": "LOOKUP", "value": "HGNC:1100"}})
544
+ - Gene_GOTerms: GO annotations for a gene
545
+ - Gene_DOTerm: Disease annotations for a gene
546
+ - GOTerm_Genes: Find genes by GO term
547
+
548
+ Use mine_list_templates to discover all available templates and their constraints.`, {
549
+ name: z.string().describe("Template name (e.g., 'Gene_Orthologs')"),
550
+ params: z
551
+ .record(z.union([
552
+ z.string(),
553
+ z.object({
554
+ path: z.string().optional(),
555
+ op: z.string().optional(),
556
+ value: z.string(),
557
+ }),
558
+ ]))
559
+ .describe("Template parameters with numeric keys, e.g., {'1': {'path': 'Gene', 'op': 'LOOKUP', 'value': 'HGNC:1100'}}"),
560
+ limit: z.number().optional().default(100).describe("Maximum results"),
561
+ }, async ({ name, params, limit }) => {
562
+ try {
563
+ const results = await client.runTemplate(name, params, limit);
564
+ return {
565
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
566
+ };
567
+ }
568
+ catch (error) {
569
+ return {
570
+ content: [{ type: "text", text: `Template error: ${error}` }],
571
+ isError: true,
572
+ };
573
+ }
574
+ });
575
+ // Tool: Get Lists
576
+ server.tool("mine_get_lists", "Get all available gene/protein lists in AllianceMine.", {
577
+ type: z
578
+ .string()
579
+ .optional()
580
+ .describe("Filter by type: Gene, Protein, etc."),
581
+ }, async ({ type }) => {
582
+ try {
583
+ const lists = await client.getLists(type);
584
+ return {
585
+ content: [{ type: "text", text: JSON.stringify(lists, null, 2) }],
586
+ };
587
+ }
588
+ catch (error) {
589
+ return {
590
+ content: [{ type: "text", text: `Error fetching lists: ${error}` }],
591
+ isError: true,
592
+ };
593
+ }
594
+ });
595
+ // Tool: Get List Contents
596
+ server.tool("mine_get_list", "Get the contents of a specific list.", {
597
+ name: z.string().describe("List name"),
598
+ }, async ({ name }) => {
599
+ try {
600
+ const data = await client.getList(name);
601
+ if (!data) {
602
+ return {
603
+ content: [{ type: "text", text: `List not found: ${name}` }],
604
+ isError: true,
605
+ };
606
+ }
607
+ return {
608
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
609
+ };
610
+ }
611
+ catch (error) {
612
+ return {
613
+ content: [{ type: "text", text: `Error fetching list: ${error}` }],
614
+ isError: true,
615
+ };
616
+ }
617
+ });
618
+ // Tool: Create List (requires auth)
619
+ server.tool("mine_create_list", "Create a new list in AllianceMine. Requires ALLIANCEMINE_TOKEN environment variable.", {
620
+ name: z.string().describe("List name"),
621
+ type: z.string().describe("List type: Gene, Protein, etc."),
622
+ identifiers: z
623
+ .array(z.string())
624
+ .describe("Array of identifiers to add to the list"),
625
+ description: z.string().optional().describe("List description"),
626
+ }, async ({ name, type, identifiers, description }) => {
627
+ try {
628
+ const result = await client.createList(name, type, identifiers, description);
629
+ return {
630
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
631
+ };
632
+ }
633
+ catch (error) {
634
+ return {
635
+ content: [{ type: "text", text: `Error creating list: ${error}` }],
636
+ isError: true,
637
+ };
638
+ }
639
+ });
640
+ // Tool: Add to List (requires auth)
641
+ server.tool("mine_add_to_list", "Add items to an existing list. Requires ALLIANCEMINE_TOKEN environment variable.", {
642
+ name: z.string().describe("List name"),
643
+ identifiers: z.array(z.string()).describe("Identifiers to add"),
644
+ }, async ({ name, identifiers }) => {
645
+ try {
646
+ const result = await client.addToList(name, identifiers);
647
+ return {
648
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
649
+ };
650
+ }
651
+ catch (error) {
652
+ return {
653
+ content: [{ type: "text", text: `Error adding to list: ${error}` }],
654
+ isError: true,
655
+ };
656
+ }
657
+ });
658
+ // Tool: Delete List (requires auth)
659
+ server.tool("mine_delete_list", "Delete a list from AllianceMine. Requires ALLIANCEMINE_TOKEN environment variable.", {
660
+ name: z.string().describe("List name to delete"),
661
+ }, async ({ name }) => {
662
+ try {
663
+ const result = await client.deleteList(name);
664
+ return {
665
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
666
+ };
667
+ }
668
+ catch (error) {
669
+ return {
670
+ content: [{ type: "text", text: `Error deleting list: ${error}` }],
671
+ isError: true,
672
+ };
673
+ }
674
+ });
237
675
  // Resource: Entity types
238
676
  server.resource("entity-types", "agr://entity-types", async () => ({
239
677
  contents: [
package/dist/types.d.ts CHANGED
@@ -160,6 +160,57 @@ export interface AllianceMineResponse {
160
160
  totalHits: number;
161
161
  facets?: Record<string, unknown>;
162
162
  }
163
+ export type QueryOperator = "=" | "!=" | "CONTAINS" | "LIKE" | "<" | ">" | "<=" | ">=" | "ONE OF" | "NONE OF" | "IS NULL" | "IS NOT NULL";
164
+ export interface QueryConstraint {
165
+ op: QueryOperator;
166
+ value: string | string[];
167
+ }
168
+ export interface QuerySort {
169
+ field: string;
170
+ direction: "ASC" | "DESC";
171
+ }
172
+ export interface QueryBuilder {
173
+ from: string;
174
+ select: string[];
175
+ where?: Record<string, string | QueryConstraint>;
176
+ joins?: string[];
177
+ sort?: QuerySort;
178
+ limit?: number;
179
+ }
180
+ export interface PathQueryResult {
181
+ results: Record<string, unknown>[];
182
+ columnHeaders?: string[];
183
+ rootClass?: string;
184
+ }
185
+ export interface MineTemplate {
186
+ name: string;
187
+ title: string;
188
+ description?: string;
189
+ rank?: number;
190
+ constraintLogic?: string;
191
+ constraints?: Record<string, unknown>[];
192
+ }
193
+ export interface TemplateParameter {
194
+ name: string;
195
+ path: string;
196
+ op: string;
197
+ value?: string;
198
+ code?: string;
199
+ }
200
+ export interface MineList {
201
+ name: string;
202
+ type: string;
203
+ size: number;
204
+ description?: string;
205
+ dateCreated?: string;
206
+ authorized?: boolean;
207
+ }
208
+ export interface ListContents {
209
+ name: string;
210
+ type: string;
211
+ size: number;
212
+ results: Record<string, unknown>[];
213
+ }
163
214
  export interface APIError {
164
215
  status: number;
165
216
  message: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agr-mcp-server",
3
- "version": "4.0.3",
3
+ "version": "5.0.2",
4
4
  "description": "MCP server for Alliance of Genome Resources - access genomics data across model organisms",
5
5
  "main": "dist/index.js",
6
6
  "bin": {