@digital-science-dsl/dimensions-analytics-mcp 1.0.4 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/dsl/schema/structured-entities.d.ts +1 -1
  3. package/dist/dsl/schema/structured-entities.d.ts.map +1 -1
  4. package/dist/dsl/schema/structured-entities.js +4 -0
  5. package/dist/dsl/schema/structured-entities.js.map +1 -1
  6. package/dist/dsl/types/vocabulary.d.ts +2 -2
  7. package/dist/dsl/types/vocabulary.d.ts.map +1 -1
  8. package/dist/dsl/types/vocabulary.js +5 -0
  9. package/dist/dsl/types/vocabulary.js.map +1 -1
  10. package/dist/mcp/examples/dsl-examples.d.ts.map +1 -1
  11. package/dist/mcp/examples/dsl-examples.js +14 -0
  12. package/dist/mcp/examples/dsl-examples.js.map +1 -1
  13. package/dist/mcp/examples/usage-scenarios.d.ts.map +1 -1
  14. package/dist/mcp/examples/usage-scenarios.js +19 -0
  15. package/dist/mcp/examples/usage-scenarios.js.map +1 -1
  16. package/dist/mcp/middleware/field-aliases.d.ts.map +1 -1
  17. package/dist/mcp/middleware/field-aliases.js +4 -0
  18. package/dist/mcp/middleware/field-aliases.js.map +1 -1
  19. package/dist/mcp/server.d.ts.map +1 -1
  20. package/dist/mcp/server.js +8 -2
  21. package/dist/mcp/server.js.map +1 -1
  22. package/dist/mcp/tools/analytics-filters.d.ts.map +1 -1
  23. package/dist/mcp/tools/analytics-filters.js +2 -0
  24. package/dist/mcp/tools/analytics-filters.js.map +1 -1
  25. package/dist/mcp/tools/lookup.js +1 -1
  26. package/dist/mcp/tools/lookup.js.map +1 -1
  27. package/dist/mcp/tools/search-entity-metadata.d.ts.map +1 -1
  28. package/dist/mcp/tools/search-entity-metadata.js +46 -0
  29. package/dist/mcp/tools/search-entity-metadata.js.map +1 -1
  30. package/dist/mcp/tools/similar-documents.d.ts +26 -0
  31. package/dist/mcp/tools/similar-documents.d.ts.map +1 -0
  32. package/dist/mcp/tools/similar-documents.js +171 -0
  33. package/dist/mcp/tools/similar-documents.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/dsl/schema/structured-entities.ts +4 -0
  36. package/src/dsl/types/vocabulary.ts +12 -2
  37. package/src/mcp/examples/dsl-examples.ts +14 -0
  38. package/src/mcp/examples/usage-scenarios.ts +19 -0
  39. package/src/mcp/middleware/field-aliases.ts +4 -0
  40. package/src/mcp/server.ts +8 -2
  41. package/src/mcp/tools/analytics-filters.ts +2 -0
  42. package/src/mcp/tools/lookup.ts +1 -1
  43. package/src/mcp/tools/search-entity-metadata.ts +50 -0
  44. package/src/mcp/tools/similar-documents.ts +232 -0
  45. package/test/examples/usage-scenarios.test.ts +5 -3
  46. package/test/fixtures/describe-schema.json +74 -0
  47. package/test/integration/suites/index.ts +2 -0
  48. package/test/integration/suites/search.integration.ts +24 -0
  49. package/test/integration/suites/similar-documents.integration.ts +51 -0
  50. package/test/schema/store.test.ts +5 -0
  51. package/test/server.test.ts +5 -0
  52. package/test/tools/search.test.ts +42 -2
  53. package/test/tools/similar-documents.test.ts +173 -0
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,171 @@
1
+ /**
2
+ * similar_documents MCP tool — find publications/grants with similar topics via DSL.
3
+ * Uses concept extraction + weighted concepts search (not vector embeddings).
4
+ * @module mcp/tools/similar-documents
5
+ */
6
+ import { z } from "zod";
7
+ import { applyFilters, ExtendedWhereFilterSchema, parseEntityResponse, resolveSkipAndLimit, SCHEMA_LIMITS, searchResultKey, validateSearchPaginationPolicy, } from "../../dsl/index.js";
8
+ import { withFieldAliases } from "../middleware/field-aliases.js";
9
+ import { registerTrackedTool } from "../usage-tracking.js";
10
+ import { formatErrorResult, formatToolResult, READ_ONLY_API_ANNOTATIONS, withSearchPagination, } from "../utils.js";
11
+ import { PAGINATION_INPUT_SCHEMA, PAGINATION_OUTPUT_SCHEMA } from "./search-input.js";
12
+ /** Entity types that support the DSL similar_documents() search function. */
13
+ export const SIMILAR_DOCUMENTS_ENTITY_TYPES = ["publications", "grants"];
14
+ /** Year filter field per entity (publications use year; grants use start_year). */
15
+ const YEAR_FIELD = {
16
+ publications: "year",
17
+ grants: "start_year",
18
+ };
19
+ /**
20
+ * Builds DSL for a similar_documents search from tool arguments.
21
+ * @param client - Dimensions client
22
+ * @param entityType - publications or grants
23
+ * @param args - Tool arguments (after field-alias resolution)
24
+ * @returns DSL query string
25
+ */
26
+ export function buildSimilarDocumentsDsl(client, entityType, args) {
27
+ const text = String(args.text ?? "");
28
+ const builder = client.createQueryBuilder().search(entityType).forSimilar(text);
29
+ const yearField = YEAR_FIELD[entityType];
30
+ if (typeof args.yearFrom === "number") {
31
+ builder.where(yearField, ">=", args.yearFrom);
32
+ }
33
+ if (typeof args.yearTo === "number") {
34
+ builder.where(yearField, "<=", args.yearTo);
35
+ }
36
+ const filters = args.filters;
37
+ if (filters?.length) {
38
+ applyFilters(builder, filters);
39
+ }
40
+ const fields = args.fields;
41
+ if (fields?.length) {
42
+ builder.fields(fields);
43
+ }
44
+ const sortBy = typeof args.sortBy === "string" ? args.sortBy : "score";
45
+ builder.sort(sortBy, "desc");
46
+ // Default to 20 for similarity (top-N relevance), not the search_* default of 100.
47
+ const { skip, limit } = resolveSkipAndLimit({
48
+ skip: args.skip,
49
+ page: args.page,
50
+ limit: args.limit ?? 20,
51
+ pageSize: args.pageSize,
52
+ });
53
+ builder.limit(limit);
54
+ if (skip > 0) {
55
+ builder.skip(skip);
56
+ }
57
+ return builder.build();
58
+ }
59
+ /**
60
+ * Registers the similar_documents tool with the MCP server.
61
+ * @param server - MCP server instance
62
+ * @param client - Dimensions client instance
63
+ */
64
+ export function registerSimilarDocumentsTool(server, client) {
65
+ registerTrackedTool(server, "similar_documents", {
66
+ description: "Find publications or grants with similar research topics based on abstract/description text. " +
67
+ "Uses Dimensions concept extraction and weighted concepts search (not vector embeddings). " +
68
+ "Prefer this over execute_dsl for similarity. For a known record: get_by_id / get_by_doi first, " +
69
+ "then pass its abstract/description as text. Supported entityType: publications, grants only.",
70
+ inputSchema: {
71
+ entityType: z
72
+ .enum(SIMILAR_DOCUMENTS_ENTITY_TYPES)
73
+ .describe("Entity type to search (publications or grants)"),
74
+ text: z
75
+ .string()
76
+ .min(1)
77
+ .describe("Abstract, description, or other prose to find similar documents for. " +
78
+ "Longer topical text works better than short keywords."),
79
+ limit: z
80
+ .number()
81
+ .int()
82
+ .min(1)
83
+ .max(SCHEMA_LIMITS.maxLimit)
84
+ .default(20)
85
+ .describe(`Maximum results to return (max ${SCHEMA_LIMITS.maxLimit}, default 20)`),
86
+ ...PAGINATION_INPUT_SCHEMA,
87
+ fields: z
88
+ .array(z.string())
89
+ .optional()
90
+ .describe("Fields to return. Accepts aliases or DSL names. Use dimensions://fields/{entity} for the full list."),
91
+ filters: z
92
+ .array(ExtendedWhereFilterSchema)
93
+ .optional()
94
+ .describe("Additional where-clause filters"),
95
+ yearFrom: z
96
+ .number()
97
+ .int()
98
+ .optional()
99
+ .describe("Filter from this year inclusive (publications: year; grants: start_year)"),
100
+ yearTo: z
101
+ .number()
102
+ .int()
103
+ .optional()
104
+ .describe("Filter up to this year inclusive (publications: year; grants: start_year)"),
105
+ sortBy: z
106
+ .string()
107
+ .optional()
108
+ .describe("Sort field (default: score for relevance). Examples: score, times_cited, year, start_year."),
109
+ confirmLargeFetch: z
110
+ .boolean()
111
+ .optional()
112
+ .default(false)
113
+ .describe("Required when skip≥5000, page≥5, or limit=1000 with skip>0. See dimensions://schema/policy."),
114
+ },
115
+ outputSchema: {
116
+ entityType: z.enum(SIMILAR_DOCUMENTS_ENTITY_TYPES).describe("Entity type searched"),
117
+ totalCount: z.number().describe("Total matching records"),
118
+ returnedCount: z.number().describe("Records returned in this response"),
119
+ truncated: z.boolean().optional().describe("True when more results exist beyond this page"),
120
+ truncationWarning: z.string().optional().describe("Warning message when truncated"),
121
+ ...PAGINATION_OUTPUT_SCHEMA,
122
+ publications: z
123
+ .array(z.record(z.string(), z.unknown()))
124
+ .optional()
125
+ .describe("Matching publications (when entityType is publications)"),
126
+ grants: z
127
+ .array(z.record(z.string(), z.unknown()))
128
+ .optional()
129
+ .describe("Matching grants (when entityType is grants)"),
130
+ },
131
+ annotations: READ_ONLY_API_ANNOTATIONS,
132
+ }, withFieldAliases({
133
+ entitySource: { kind: "dynamic", argName: "entityType" },
134
+ fieldArrayArgs: ["fields"],
135
+ fieldStringArgs: ["sortBy"],
136
+ filterArrayArgs: ["filters"],
137
+ }, async (args) => {
138
+ try {
139
+ const entityType = args.entityType;
140
+ const record = args;
141
+ const { skip, limit } = resolveSkipAndLimit({
142
+ skip: record.skip,
143
+ page: record.page,
144
+ limit: record.limit ?? 20,
145
+ });
146
+ validateSearchPaginationPolicy({
147
+ skip,
148
+ limit,
149
+ confirmLargeFetch: record.confirmLargeFetch,
150
+ });
151
+ const dsl = buildSimilarDocumentsDsl(client, entityType, {
152
+ ...record,
153
+ limit,
154
+ });
155
+ const response = (await client.rawQuery(dsl));
156
+ const parsed = parseEntityResponse(response, entityType);
157
+ const rows = parsed.data;
158
+ const resultKey = searchResultKey(entityType);
159
+ return formatToolResult(withSearchPagination({
160
+ entityType,
161
+ totalCount: parsed.totalCount,
162
+ returnedCount: rows.length,
163
+ [resultKey]: rows,
164
+ }, parsed.totalCount, rows.length, skip, limit));
165
+ }
166
+ catch (error) {
167
+ return formatErrorResult(error);
168
+ }
169
+ }));
170
+ }
171
+ //# sourceMappingURL=similar-documents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similar-documents.js","sourceRoot":"","sources":["../../../src/mcp/tools/similar-documents.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digital-science-dsl/dimensions-analytics-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "description": "Dimensions Analytics MCP server for Dimensions DSL querying",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,6 +13,10 @@ export const STRUCTURED_ENTITY_TYPES = [
13
13
  "datasets",
14
14
  "policy_documents",
15
15
  "organizations",
16
+ "reports",
17
+ "source_titles",
18
+ "funder_groups",
19
+ "research_org_groups",
16
20
  ] as const;
17
21
 
18
22
  /** A source with a structured search tool. */
@@ -18,7 +18,11 @@ export type EntityType =
18
18
  | "clinical_trials"
19
19
  | "datasets"
20
20
  | "policy_documents"
21
- | "organizations";
21
+ | "organizations"
22
+ | "reports"
23
+ | "source_titles"
24
+ | "funder_groups"
25
+ | "research_org_groups";
22
26
 
23
27
  /** Search indexes for `search <source> in <index> for "..."` clauses. */
24
28
  export type SearchIndex =
@@ -34,7 +38,8 @@ export type SearchIndex =
34
38
  | "acknowledgements"
35
39
  | "raw_affiliations"
36
40
  | "assignees"
37
- | "title_abstract_claims";
41
+ | "title_abstract_claims"
42
+ | "name";
38
43
 
39
44
  /** Sort direction for order-by clauses. */
40
45
  export type SortOrder = "asc" | "desc";
@@ -62,6 +67,10 @@ export const VALID_ENTITIES: readonly EntityType[] = [
62
67
  "datasets",
63
68
  "policy_documents",
64
69
  "organizations",
70
+ "reports",
71
+ "source_titles",
72
+ "funder_groups",
73
+ "research_org_groups",
65
74
  ] as const;
66
75
 
67
76
  /** Valid search indexes for runtime validation. */
@@ -79,4 +88,5 @@ export const VALID_INDEXES: readonly SearchIndex[] = [
79
88
  "raw_affiliations",
80
89
  "assignees",
81
90
  "title_abstract_claims",
91
+ "name",
82
92
  ] as const;
@@ -12,11 +12,13 @@ export const DSL_EXAMPLES_BY_ENTITY: Record<string, readonly string[]> = {
12
12
  'search publications for "machine learning" return publications[id+title+doi] limit 10',
13
13
  'search publications for "CRISPR" where year >= 2020 return publications[id+title+times_cited+year] sort by times_cited desc limit 25',
14
14
  'search publications in title_only for "gene therapy" return publications[basics] limit 10',
15
+ 'search publications for similar_documents("After spinal cord injury, macrophages infiltrate the lesion site") where year > 2015 return publications[id+title+year] sort by score desc limit 5',
15
16
  "search publications return publications facet journal limit 20",
16
17
  ],
17
18
  grants: [
18
19
  'search grants for "cancer immunotherapy" return grants limit 10',
19
20
  'search grants where funder_org_name = "National Institutes of Health" return grants[basics] limit 10',
21
+ 'search grants for similar_documents("Cancer immunotherapy with checkpoint inhibitors and CAR-T") where start_year >= 2020 return grants[id+title] sort by score desc limit 5',
20
22
  "search grants return grants aggregate funding_usd",
21
23
  ],
22
24
  patents: [
@@ -39,6 +41,18 @@ export const DSL_EXAMPLES_BY_ENTITY: Record<string, readonly string[]> = {
39
41
  'search organizations for "Stanford" return organizations limit 10',
40
42
  'search organizations where country = "United States" return organizations limit 10',
41
43
  ],
44
+ reports: [
45
+ 'search reports for "climate change" return reports[basics] limit 10',
46
+ "search reports where year >= 2020 return reports[id+title+year] limit 20",
47
+ ],
48
+ source_titles: [
49
+ 'search source_titles for "Nature" return source_titles[basics] limit 10',
50
+ 'search source_titles where type = "journal" return source_titles[id+title+issn+sjr] limit 20',
51
+ ],
52
+ funder_groups: ['search funder_groups for "NIH" return funder_groups[basics] limit 10'],
53
+ research_org_groups: [
54
+ 'search research_org_groups for "Ivy League" return research_org_groups[basics] limit 10',
55
+ ],
42
56
  };
43
57
 
44
58
  /** General DSL patterns (describe, functions, pagination). */
@@ -164,6 +164,25 @@ export const USAGE_SCENARIOS: readonly UsageScenario[] = [
164
164
  },
165
165
  dslContains: ["large language model", "citations_per_year(2018, 2024)"],
166
166
  },
167
+ {
168
+ id: "similar-documents",
169
+ goal: "Similar papers from an abstract",
170
+ prompt: "Find publications similar to this abstract about spinal cord injury macrophages.",
171
+ tool: "similar_documents",
172
+ args: {
173
+ entityType: "publications",
174
+ text: "After spinal cord injury, macrophages infiltrate the lesion site and contribute to both tissue damage and repair processes.",
175
+ yearFrom: 2016,
176
+ limit: 10,
177
+ fields: ["id", "title", "year"],
178
+ },
179
+ dslContains: [
180
+ "search publications for similar_documents(",
181
+ "year >= 2016",
182
+ "sort by score desc",
183
+ "limit 10",
184
+ ],
185
+ },
167
186
  {
168
187
  id: "build-complex-query",
169
188
  goal: "Build a complex query",
@@ -103,6 +103,10 @@ export const ENTITY_ALIASES: Readonly<Record<EntityType, Readonly<Record<string,
103
103
  policy_documents: { ...POLICY_DOCUMENT_ALIASES },
104
104
  researchers: { ...RESEARCHER_ALIASES },
105
105
  organizations: { ...ORGANIZATION_ALIASES },
106
+ reports: {},
107
+ source_titles: {},
108
+ funder_groups: {},
109
+ research_org_groups: {},
106
110
  };
107
111
 
108
112
  // ---------------------------------------------------------------------------
package/src/mcp/server.ts CHANGED
@@ -27,6 +27,7 @@ import { registerLookupTools } from "./tools/lookup.js";
27
27
  import { registerQueryTools } from "./tools/query.js";
28
28
  import { registerSchemaTools, validateFieldAliases } from "./tools/schema.js";
29
29
  import { registerSearchTools } from "./tools/search.js";
30
+ import { registerSimilarDocumentsTool } from "./tools/similar-documents.js";
30
31
  import { initMcpUsageTracking } from "./usage-tracking.js";
31
32
 
32
33
  /**
@@ -94,7 +95,7 @@ export function buildServerInstructions(schemaStore: SchemaStore): string {
94
95
  [
95
96
  "Workflow:",
96
97
  "(1) Discover schema — read dimensions://schema/summary, dimensions://fields/{entity}, dimensions://examples/{source}, or describe_schema;",
97
- "(2) Search — search_* for keyword discovery with filters; get_by_doi, get_by_pmid, get_by_id for known identifiers;",
98
+ "(2) Search — search_* for keyword discovery with filters; similar_documents for topic-similar pubs/grants from abstract text; get_by_doi, get_by_pmid, get_by_id for known identifiers;",
98
99
  "(3) Analyze — facet_query, aggregate_query, citation_trend, funding_trend;",
99
100
  "(4) Drill down — get_by_id, search_* with filters (e.g. researchers.id, research_orgs.id), facet_query for top researchers at an org.",
100
101
  ].join(" "),
@@ -103,12 +104,16 @@ export function buildServerInstructions(schemaStore: SchemaStore): string {
103
104
  "search_researchers matches names only — topic→researcher uses facet_query (entityType publications, facetField researchers);",
104
105
  "search_grants funderOrgName needs exact Dimensions names (NCI/NSF acronyms resolve; discover funders via facet_query/aggregate_query on facetField funder_orgs);",
105
106
  "search_organizations for institution lookup — prefer GRID id in filters when the name is ambiguous;",
107
+ "search_source_titles for journals / ISSN lookup (not articles); search_reports for technical reports;",
108
+ "search_funder_groups / search_research_org_groups for curated group name → member GRID ids;",
109
+ "similar_documents finds concept-similar publications/grants from prose (not embeddings); for a known ID, get_by_id then pass abstract/description as text;",
106
110
  "facet_query supports yearFrom/yearTo for year-scoped facets.",
107
111
  ].join(" "),
108
112
  [
109
113
  "Query construction:",
110
114
  "for ranked publication search (e.g. most-cited since 2020), use search_publications with query, yearFrom, sortBy (times_cited or total_citations), limit — do not hand-write execute_dsl;",
111
- "use execute_dsl for boolean concept groups and DSL special functions;",
115
+ "for similar-document lookup, use similar_documents — do not hand-write similar_documents() in execute_dsl;",
116
+ "use execute_dsl for boolean concept groups and remaining DSL special functions (classify, extract_concepts);",
112
117
  "use execute_dsl only when structured tools are insufficient; DSL order is return ... sort by FIELD order limit N (never limit before sort).",
113
118
  ].join(" "),
114
119
  [
@@ -136,6 +141,7 @@ function registerAllTools(
136
141
  registerLookupTools(server, client);
137
142
  registerQueryTools(server, client, schemaStore);
138
143
  registerFunctionTools(server, client);
144
+ registerSimilarDocumentsTool(server, client);
139
145
  registerAnalyticsTools(server, client, schemaStore);
140
146
  registerSchemaTools(server, client, schemaContext);
141
147
  }
@@ -14,6 +14,8 @@ const ENTITY_YEAR_FIELD: Partial<Record<EntityType, string>> = {
14
14
  clinical_trials: "year",
15
15
  datasets: "year",
16
16
  policy_documents: "year",
17
+ reports: "year",
18
+ source_titles: "start_year",
17
19
  };
18
20
 
19
21
  /**
@@ -136,7 +136,7 @@ export function registerLookupTools(server: McpServer, client: DimensionsClient)
136
136
  "get_by_id",
137
137
  {
138
138
  description:
139
- "Retrieve any entity by its Dimensions ID. Supports publications, grants, patents, clinical trials, datasets, policy documents, researchers, and organizations.",
139
+ "Retrieve any entity by its Dimensions ID. Supports publications, grants, patents, clinical trials, datasets, policy documents, researchers, organizations, reports, source titles, funder groups, and research org groups.",
140
140
  inputSchema: {
141
141
  entityType: EntitySchema.describe("The type of entity to look up"),
142
142
  id: z.string().describe("The Dimensions ID to look up"),
@@ -250,4 +250,54 @@ export const SEARCH_ENTITY_METADATA: readonly SearchEntityMetadata[] = [
250
250
  sortBy: z.enum(["name"]).optional().describe("Sort results by this field"),
251
251
  },
252
252
  },
253
+ {
254
+ source: "reports",
255
+ description:
256
+ "Search technical reports in the Dimensions database. Returns report titles, abstracts, responsible organizations, funders, and related metadata.",
257
+ applyConvenienceFilters: (builder, args) => {
258
+ yearRange("yearFrom", "yearTo", "year")(builder, args);
259
+ },
260
+ extraInputSchema: {
261
+ yearFrom: z.number().int().optional().describe("Filter reports from this year (inclusive)"),
262
+ yearTo: z.number().int().optional().describe("Filter reports up to this year (inclusive)"),
263
+ sortBy: z.enum(["year", "date"]).optional().describe("Sort results by this field"),
264
+ },
265
+ },
266
+ {
267
+ source: "source_titles",
268
+ description:
269
+ "Search publication source titles (journals, preprint platforms, book series, proceedings) in the Dimensions database. " +
270
+ "Use for journal / ISSN lookup — not for searching articles (use search_publications).",
271
+ applyConvenienceFilters: (builder, args) => {
272
+ if (typeof args.sourceTitleType === "string") {
273
+ builder.where("type", "=", args.sourceTitleType);
274
+ }
275
+ },
276
+ extraInputSchema: {
277
+ sourceTitleType: z
278
+ .enum(["journal", "book_series", "proceeding", "preprint_platform"])
279
+ .optional()
280
+ .describe("Filter by source title type"),
281
+ sortBy: z
282
+ .enum(["sjr", "snip", "start_year"])
283
+ .optional()
284
+ .describe("Sort results by this field"),
285
+ },
286
+ },
287
+ {
288
+ source: "funder_groups",
289
+ description:
290
+ "Search curated funder groups in the Dimensions database. Returns group name and member organization GRID IDs. " +
291
+ "Query matches group names (search index: name).",
292
+ applyConvenienceFilters: noop,
293
+ extraInputSchema: {},
294
+ },
295
+ {
296
+ source: "research_org_groups",
297
+ description:
298
+ "Search curated research organization groups in the Dimensions database. Returns group name and member organization GRID IDs. " +
299
+ "Query matches group names (search index: name).",
300
+ applyConvenienceFilters: noop,
301
+ extraInputSchema: {},
302
+ },
253
303
  ];
@@ -0,0 +1,232 @@
1
+ /**
2
+ * similar_documents MCP tool — find publications/grants with similar topics via DSL.
3
+ * Uses concept extraction + weighted concepts search (not vector embeddings).
4
+ * @module mcp/tools/similar-documents
5
+ */
6
+
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+ import type { DimensionsClient } from "../../dsl/index.js";
10
+ import {
11
+ applyFilters,
12
+ type ExtendedWhereFilterInput,
13
+ ExtendedWhereFilterSchema,
14
+ parseEntityResponse,
15
+ resolveSkipAndLimit,
16
+ SCHEMA_LIMITS,
17
+ type StructuredEntityType,
18
+ searchResultKey,
19
+ validateSearchPaginationPolicy,
20
+ } from "../../dsl/index.js";
21
+ import { withFieldAliases } from "../middleware/field-aliases.js";
22
+ import { registerTrackedTool } from "../usage-tracking.js";
23
+ import {
24
+ formatErrorResult,
25
+ formatToolResult,
26
+ READ_ONLY_API_ANNOTATIONS,
27
+ withSearchPagination,
28
+ } from "../utils.js";
29
+ import { PAGINATION_INPUT_SCHEMA, PAGINATION_OUTPUT_SCHEMA } from "./search-input.js";
30
+
31
+ /** Entity types that support the DSL similar_documents() search function. */
32
+ export const SIMILAR_DOCUMENTS_ENTITY_TYPES = ["publications", "grants"] as const;
33
+
34
+ /** Entity type supported by similar_documents. */
35
+ export type SimilarDocumentsEntityType = (typeof SIMILAR_DOCUMENTS_ENTITY_TYPES)[number];
36
+
37
+ /** Year filter field per entity (publications use year; grants use start_year). */
38
+ const YEAR_FIELD: Record<SimilarDocumentsEntityType, string> = {
39
+ publications: "year",
40
+ grants: "start_year",
41
+ };
42
+
43
+ /**
44
+ * Builds DSL for a similar_documents search from tool arguments.
45
+ * @param client - Dimensions client
46
+ * @param entityType - publications or grants
47
+ * @param args - Tool arguments (after field-alias resolution)
48
+ * @returns DSL query string
49
+ */
50
+ export function buildSimilarDocumentsDsl(
51
+ client: DimensionsClient,
52
+ entityType: SimilarDocumentsEntityType,
53
+ args: Record<string, unknown>,
54
+ ): string {
55
+ const text = String(args.text ?? "");
56
+ const builder = client.createQueryBuilder().search(entityType).forSimilar(text);
57
+
58
+ const yearField = YEAR_FIELD[entityType];
59
+ if (typeof args.yearFrom === "number") {
60
+ builder.where(yearField, ">=", args.yearFrom);
61
+ }
62
+ if (typeof args.yearTo === "number") {
63
+ builder.where(yearField, "<=", args.yearTo);
64
+ }
65
+
66
+ const filters = args.filters as ExtendedWhereFilterInput[] | undefined;
67
+ if (filters?.length) {
68
+ applyFilters(builder, filters);
69
+ }
70
+
71
+ const fields = args.fields as string[] | undefined;
72
+ if (fields?.length) {
73
+ builder.fields(fields);
74
+ }
75
+
76
+ const sortBy = typeof args.sortBy === "string" ? args.sortBy : "score";
77
+ builder.sort(sortBy, "desc");
78
+
79
+ // Default to 20 for similarity (top-N relevance), not the search_* default of 100.
80
+ const { skip, limit } = resolveSkipAndLimit({
81
+ skip: args.skip as number | undefined,
82
+ page: args.page as number | undefined,
83
+ limit: (args.limit as number | undefined) ?? 20,
84
+ pageSize: args.pageSize as number | undefined,
85
+ });
86
+ builder.limit(limit);
87
+ if (skip > 0) {
88
+ builder.skip(skip);
89
+ }
90
+
91
+ return builder.build();
92
+ }
93
+
94
+ /**
95
+ * Registers the similar_documents tool with the MCP server.
96
+ * @param server - MCP server instance
97
+ * @param client - Dimensions client instance
98
+ */
99
+ export function registerSimilarDocumentsTool(server: McpServer, client: DimensionsClient): void {
100
+ registerTrackedTool(
101
+ server,
102
+ "similar_documents",
103
+ {
104
+ description:
105
+ "Find publications or grants with similar research topics based on abstract/description text. " +
106
+ "Uses Dimensions concept extraction and weighted concepts search (not vector embeddings). " +
107
+ "Prefer this over execute_dsl for similarity. For a known record: get_by_id / get_by_doi first, " +
108
+ "then pass its abstract/description as text. Supported entityType: publications, grants only.",
109
+ inputSchema: {
110
+ entityType: z
111
+ .enum(SIMILAR_DOCUMENTS_ENTITY_TYPES)
112
+ .describe("Entity type to search (publications or grants)"),
113
+ text: z
114
+ .string()
115
+ .min(1)
116
+ .describe(
117
+ "Abstract, description, or other prose to find similar documents for. " +
118
+ "Longer topical text works better than short keywords.",
119
+ ),
120
+ limit: z
121
+ .number()
122
+ .int()
123
+ .min(1)
124
+ .max(SCHEMA_LIMITS.maxLimit)
125
+ .default(20)
126
+ .describe(`Maximum results to return (max ${SCHEMA_LIMITS.maxLimit}, default 20)`),
127
+ ...PAGINATION_INPUT_SCHEMA,
128
+ fields: z
129
+ .array(z.string())
130
+ .optional()
131
+ .describe(
132
+ "Fields to return. Accepts aliases or DSL names. Use dimensions://fields/{entity} for the full list.",
133
+ ),
134
+ filters: z
135
+ .array(ExtendedWhereFilterSchema)
136
+ .optional()
137
+ .describe("Additional where-clause filters"),
138
+ yearFrom: z
139
+ .number()
140
+ .int()
141
+ .optional()
142
+ .describe("Filter from this year inclusive (publications: year; grants: start_year)"),
143
+ yearTo: z
144
+ .number()
145
+ .int()
146
+ .optional()
147
+ .describe("Filter up to this year inclusive (publications: year; grants: start_year)"),
148
+ sortBy: z
149
+ .string()
150
+ .optional()
151
+ .describe(
152
+ "Sort field (default: score for relevance). Examples: score, times_cited, year, start_year.",
153
+ ),
154
+ confirmLargeFetch: z
155
+ .boolean()
156
+ .optional()
157
+ .default(false)
158
+ .describe(
159
+ "Required when skip≥5000, page≥5, or limit=1000 with skip>0. See dimensions://schema/policy.",
160
+ ),
161
+ },
162
+ outputSchema: {
163
+ entityType: z.enum(SIMILAR_DOCUMENTS_ENTITY_TYPES).describe("Entity type searched"),
164
+ totalCount: z.number().describe("Total matching records"),
165
+ returnedCount: z.number().describe("Records returned in this response"),
166
+ truncated: z.boolean().optional().describe("True when more results exist beyond this page"),
167
+ truncationWarning: z.string().optional().describe("Warning message when truncated"),
168
+ ...PAGINATION_OUTPUT_SCHEMA,
169
+ publications: z
170
+ .array(z.record(z.string(), z.unknown()))
171
+ .optional()
172
+ .describe("Matching publications (when entityType is publications)"),
173
+ grants: z
174
+ .array(z.record(z.string(), z.unknown()))
175
+ .optional()
176
+ .describe("Matching grants (when entityType is grants)"),
177
+ },
178
+ annotations: READ_ONLY_API_ANNOTATIONS,
179
+ },
180
+ withFieldAliases(
181
+ {
182
+ entitySource: { kind: "dynamic", argName: "entityType" },
183
+ fieldArrayArgs: ["fields"],
184
+ fieldStringArgs: ["sortBy"],
185
+ filterArrayArgs: ["filters"],
186
+ },
187
+ async (args) => {
188
+ try {
189
+ const entityType = args.entityType as SimilarDocumentsEntityType;
190
+ const record = args as Record<string, unknown>;
191
+ const { skip, limit } = resolveSkipAndLimit({
192
+ skip: record.skip as number | undefined,
193
+ page: record.page as number | undefined,
194
+ limit: (record.limit as number | undefined) ?? 20,
195
+ });
196
+
197
+ validateSearchPaginationPolicy({
198
+ skip,
199
+ limit,
200
+ confirmLargeFetch: record.confirmLargeFetch as boolean | undefined,
201
+ });
202
+
203
+ const dsl = buildSimilarDocumentsDsl(client, entityType, {
204
+ ...record,
205
+ limit,
206
+ });
207
+ const response = (await client.rawQuery(dsl)) as Record<string, unknown>;
208
+ const parsed = parseEntityResponse(response, entityType as StructuredEntityType);
209
+ const rows = parsed.data as Record<string, unknown>[];
210
+ const resultKey = searchResultKey(entityType as StructuredEntityType);
211
+
212
+ return formatToolResult(
213
+ withSearchPagination(
214
+ {
215
+ entityType,
216
+ totalCount: parsed.totalCount,
217
+ returnedCount: rows.length,
218
+ [resultKey]: rows,
219
+ },
220
+ parsed.totalCount,
221
+ rows.length,
222
+ skip,
223
+ limit,
224
+ ),
225
+ );
226
+ } catch (error) {
227
+ return formatErrorResult(error);
228
+ }
229
+ },
230
+ ),
231
+ );
232
+ }