@digital-science-dsl/dimensions-analytics-mcp 1.1.0 → 1.3.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 (104) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/http-main.js +2 -0
  3. package/dist/http-main.js.map +1 -1
  4. package/dist/main.d.ts +0 -5
  5. package/dist/main.d.ts.map +1 -1
  6. package/dist/main.js +3 -1
  7. package/dist/main.js.map +1 -1
  8. package/dist/mcp/auto-update.d.ts +63 -0
  9. package/dist/mcp/auto-update.d.ts.map +1 -0
  10. package/dist/mcp/auto-update.js +295 -0
  11. package/dist/mcp/auto-update.js.map +1 -0
  12. package/dist/mcp/examples/dsl-examples.d.ts +2 -2
  13. package/dist/mcp/examples/dsl-examples.d.ts.map +1 -1
  14. package/dist/mcp/examples/dsl-examples.js +5 -1
  15. package/dist/mcp/examples/dsl-examples.js.map +1 -1
  16. package/dist/mcp/examples/usage-scenarios.d.ts.map +1 -1
  17. package/dist/mcp/examples/usage-scenarios.js +38 -0
  18. package/dist/mcp/examples/usage-scenarios.js.map +1 -1
  19. package/dist/mcp/http-server.js +3 -3
  20. package/dist/mcp/http-server.js.map +1 -1
  21. package/dist/mcp/middleware/field-aliases.d.ts +1 -1
  22. package/dist/mcp/middleware/field-aliases.d.ts.map +1 -1
  23. package/dist/mcp/middleware/field-aliases.js +0 -6
  24. package/dist/mcp/middleware/field-aliases.js.map +1 -1
  25. package/dist/mcp/resources/schema.d.ts +1 -1
  26. package/dist/mcp/resources/schema.d.ts.map +1 -1
  27. package/dist/mcp/resources/schema.js +11 -11
  28. package/dist/mcp/resources/schema.js.map +1 -1
  29. package/dist/mcp/server.d.ts +2 -2
  30. package/dist/mcp/server.d.ts.map +1 -1
  31. package/dist/mcp/server.js +8 -4
  32. package/dist/mcp/server.js.map +1 -1
  33. package/dist/mcp/tools/analytics.d.ts +1 -1
  34. package/dist/mcp/tools/analytics.d.ts.map +1 -1
  35. package/dist/mcp/tools/analytics.js +21 -21
  36. package/dist/mcp/tools/analytics.js.map +1 -1
  37. package/dist/mcp/tools/fetch-search-pages.d.ts +1 -1
  38. package/dist/mcp/tools/fetch-search-pages.d.ts.map +1 -1
  39. package/dist/mcp/tools/fetch-search-pages.js +4 -9
  40. package/dist/mcp/tools/fetch-search-pages.js.map +1 -1
  41. package/dist/mcp/tools/functions.d.ts +1 -1
  42. package/dist/mcp/tools/functions.d.ts.map +1 -1
  43. package/dist/mcp/tools/functions.js +8 -13
  44. package/dist/mcp/tools/functions.js.map +1 -1
  45. package/dist/mcp/tools/lookup.d.ts +1 -1
  46. package/dist/mcp/tools/lookup.d.ts.map +1 -1
  47. package/dist/mcp/tools/lookup.js +12 -17
  48. package/dist/mcp/tools/lookup.js.map +1 -1
  49. package/dist/mcp/tools/query.d.ts +1 -1
  50. package/dist/mcp/tools/query.d.ts.map +1 -1
  51. package/dist/mcp/tools/query.js +4 -9
  52. package/dist/mcp/tools/query.js.map +1 -1
  53. package/dist/mcp/tools/schema.d.ts +1 -1
  54. package/dist/mcp/tools/schema.d.ts.map +1 -1
  55. package/dist/mcp/tools/schema.js +7 -11
  56. package/dist/mcp/tools/schema.js.map +1 -1
  57. package/dist/mcp/tools/search.d.ts +1 -1
  58. package/dist/mcp/tools/search.d.ts.map +1 -1
  59. package/dist/mcp/tools/search.js +4 -9
  60. package/dist/mcp/tools/search.js.map +1 -1
  61. package/dist/mcp/tools/similar-documents.d.ts +26 -0
  62. package/dist/mcp/tools/similar-documents.d.ts.map +1 -0
  63. package/dist/mcp/tools/similar-documents.js +166 -0
  64. package/dist/mcp/tools/similar-documents.js.map +1 -0
  65. package/dist/mcp/usage-tracking.d.ts +1 -1
  66. package/dist/mcp/usage-tracking.d.ts.map +1 -1
  67. package/dist/mcp/utils.d.ts +1 -2
  68. package/dist/mcp/utils.d.ts.map +1 -1
  69. package/dist/mcp/utils.js +0 -4
  70. package/dist/mcp/utils.js.map +1 -1
  71. package/package.json +6 -2
  72. package/src/http-main.ts +3 -0
  73. package/src/main.ts +4 -2
  74. package/src/mcp/auto-update.ts +379 -0
  75. package/src/mcp/examples/dsl-examples.ts +5 -1
  76. package/src/mcp/examples/usage-scenarios.ts +38 -0
  77. package/src/mcp/http-server.ts +3 -3
  78. package/src/mcp/middleware/field-aliases.ts +1 -2
  79. package/src/mcp/resources/schema.ts +12 -12
  80. package/src/mcp/server.ts +9 -5
  81. package/src/mcp/tools/analytics.ts +26 -26
  82. package/src/mcp/tools/fetch-search-pages.ts +5 -6
  83. package/src/mcp/tools/functions.ts +9 -10
  84. package/src/mcp/tools/lookup.ts +13 -14
  85. package/src/mcp/tools/query.ts +5 -6
  86. package/src/mcp/tools/schema.ts +8 -9
  87. package/src/mcp/tools/search.ts +5 -6
  88. package/src/mcp/tools/similar-documents.ts +231 -0
  89. package/src/mcp/usage-tracking.ts +1 -1
  90. package/src/mcp/utils.ts +1 -3
  91. package/test/examples/dsl-examples.test.ts +8 -0
  92. package/test/examples/usage-scenarios.test.ts +5 -3
  93. package/test/integration/harness.ts +1 -3
  94. package/test/integration/hosted-client.ts +1 -3
  95. package/test/integration/hosted.e2e.test.ts +1 -1
  96. package/test/integration/suites/index.ts +2 -0
  97. package/test/integration/suites/similar-documents.integration.ts +51 -0
  98. package/test/integration/verify-usage-scenarios.ts +1 -3
  99. package/test/mcp/auto-update.test.ts +347 -0
  100. package/test/middleware/field-aliases.test.ts +1 -1
  101. package/test/resources/schema.test.ts +5 -1
  102. package/test/server.test.ts +17 -3
  103. package/test/tools/similar-documents.test.ts +173 -0
  104. package/tsconfig.tsbuildinfo +1 -1
@@ -2,8 +2,7 @@
2
2
  * Schema introspection and validation tools for the MCP server.
3
3
  * @module mcp/tools/schema
4
4
  */
5
-
6
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import type { McpServer } from "@modelcontextprotocol/server";
7
6
  import { z } from "zod";
8
7
  import type { DimensionsClient } from "../../dsl/index.js";
9
8
  import { buildReverseAliasMap } from "../middleware/field-aliases.js";
@@ -32,7 +31,7 @@ export function registerSchemaTools(
32
31
  "Default: compact summary with source/entity counts and resource URIs. " +
33
32
  "Use full=true or dimensions://schema for the complete describe payload. " +
34
33
  "Optionally fetch live describe for a single source or entity.",
35
- inputSchema: {
34
+ inputSchema: z.object({
36
35
  source: z.string().optional().describe("Source name (e.g. publications)"),
37
36
  entity: z.string().optional().describe("Auxiliary entity name (e.g. journals)"),
38
37
  full: z
@@ -43,8 +42,8 @@ export function registerSchemaTools(
43
42
  .boolean()
44
43
  .optional()
45
44
  .describe("When true, fetch fresh describe for source/entity from the API"),
46
- },
47
- outputSchema: {
45
+ }),
46
+ outputSchema: z.object({
48
47
  summary: z
49
48
  .record(z.string(), z.unknown())
50
49
  .optional()
@@ -55,7 +54,7 @@ export function registerSchemaTools(
55
54
  .describe("Full or sliced schema when full=true or source/entity is set"),
56
55
  version: z.string().optional().describe("DSL version"),
57
56
  stats: z.record(z.string(), z.unknown()).optional().describe("Load stats and provenance"),
58
- },
57
+ }),
59
58
  annotations: READ_ONLY_API_ANNOTATIONS,
60
59
  },
61
60
  async (args) => {
@@ -106,11 +105,11 @@ export function registerSchemaTools(
106
105
  {
107
106
  description:
108
107
  "Re-fetch describe schema from the Dimensions API, rebuild in-memory schema, and re-register MCP resources.",
109
- inputSchema: {},
110
- outputSchema: {
108
+ inputSchema: z.object({}),
109
+ outputSchema: z.object({
111
110
  refreshed: z.boolean(),
112
111
  stats: z.record(z.string(), z.unknown()),
113
- },
112
+ }),
114
113
  annotations: READ_ONLY_API_ANNOTATIONS,
115
114
  },
116
115
  async () => {
@@ -3,8 +3,7 @@
3
3
  * Registers `search_{source}` tools from describe schema and shared QueryBuilder handlers.
4
4
  * @module mcp/tools/search
5
5
  */
6
-
7
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import type { McpServer } from "@modelcontextprotocol/server";
8
7
  import { z } from "zod";
9
8
  import type { DimensionsClient } from "../../dsl/index.js";
10
9
  import {
@@ -105,11 +104,11 @@ export function registerSearchTools(
105
104
  toolName,
106
105
  {
107
106
  description: meta.description,
108
- inputSchema: {
107
+ inputSchema: z.object({
109
108
  ...SHARED_SEARCH_INPUT,
110
109
  ...meta.extraInputSchema,
111
- },
112
- outputSchema: {
110
+ }),
111
+ outputSchema: z.object({
113
112
  totalCount: z.number().describe("Total matching records"),
114
113
  returnedCount: z.number().describe("Records returned in this response"),
115
114
  truncated: z
@@ -119,7 +118,7 @@ export function registerSearchTools(
119
118
  truncationWarning: z.string().optional().describe("Warning message when truncated"),
120
119
  ...PAGINATION_OUTPUT_SCHEMA,
121
120
  [resultKey]: z.array(z.record(z.string(), z.unknown())).describe("Result records"),
122
- },
121
+ }),
123
122
  annotations: READ_ONLY_API_ANNOTATIONS,
124
123
  },
125
124
  withFieldAliases(
@@ -0,0 +1,231 @@
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 type { McpServer } from "@modelcontextprotocol/server";
7
+ import { z } from "zod";
8
+ import type { DimensionsClient } from "../../dsl/index.js";
9
+ import {
10
+ applyFilters,
11
+ type ExtendedWhereFilterInput,
12
+ ExtendedWhereFilterSchema,
13
+ parseEntityResponse,
14
+ resolveSkipAndLimit,
15
+ SCHEMA_LIMITS,
16
+ type StructuredEntityType,
17
+ searchResultKey,
18
+ validateSearchPaginationPolicy,
19
+ } from "../../dsl/index.js";
20
+ import { withFieldAliases } from "../middleware/field-aliases.js";
21
+ import { registerTrackedTool } from "../usage-tracking.js";
22
+ import {
23
+ formatErrorResult,
24
+ formatToolResult,
25
+ READ_ONLY_API_ANNOTATIONS,
26
+ withSearchPagination,
27
+ } from "../utils.js";
28
+ import { PAGINATION_INPUT_SCHEMA, PAGINATION_OUTPUT_SCHEMA } from "./search-input.js";
29
+
30
+ /** Entity types that support the DSL similar_documents() search function. */
31
+ export const SIMILAR_DOCUMENTS_ENTITY_TYPES = ["publications", "grants"] as const;
32
+
33
+ /** Entity type supported by similar_documents. */
34
+ export type SimilarDocumentsEntityType = (typeof SIMILAR_DOCUMENTS_ENTITY_TYPES)[number];
35
+
36
+ /** Year filter field per entity (publications use year; grants use start_year). */
37
+ const YEAR_FIELD: Record<SimilarDocumentsEntityType, string> = {
38
+ publications: "year",
39
+ grants: "start_year",
40
+ };
41
+
42
+ /**
43
+ * Builds DSL for a similar_documents search from tool arguments.
44
+ * @param client - Dimensions client
45
+ * @param entityType - publications or grants
46
+ * @param args - Tool arguments (after field-alias resolution)
47
+ * @returns DSL query string
48
+ */
49
+ export function buildSimilarDocumentsDsl(
50
+ client: DimensionsClient,
51
+ entityType: SimilarDocumentsEntityType,
52
+ args: Record<string, unknown>,
53
+ ): string {
54
+ const text = String(args.text ?? "");
55
+ const builder = client.createQueryBuilder().search(entityType).forSimilar(text);
56
+
57
+ const yearField = YEAR_FIELD[entityType];
58
+ if (typeof args.yearFrom === "number") {
59
+ builder.where(yearField, ">=", args.yearFrom);
60
+ }
61
+ if (typeof args.yearTo === "number") {
62
+ builder.where(yearField, "<=", args.yearTo);
63
+ }
64
+
65
+ const filters = args.filters as ExtendedWhereFilterInput[] | undefined;
66
+ if (filters?.length) {
67
+ applyFilters(builder, filters);
68
+ }
69
+
70
+ const fields = args.fields as string[] | undefined;
71
+ if (fields?.length) {
72
+ builder.fields(fields);
73
+ }
74
+
75
+ const sortBy = typeof args.sortBy === "string" ? args.sortBy : "score";
76
+ builder.sort(sortBy, "desc");
77
+
78
+ // Default to 20 for similarity (top-N relevance), not the search_* default of 100.
79
+ const { skip, limit } = resolveSkipAndLimit({
80
+ skip: args.skip as number | undefined,
81
+ page: args.page as number | undefined,
82
+ limit: (args.limit as number | undefined) ?? 20,
83
+ pageSize: args.pageSize as number | undefined,
84
+ });
85
+ builder.limit(limit);
86
+ if (skip > 0) {
87
+ builder.skip(skip);
88
+ }
89
+
90
+ return builder.build();
91
+ }
92
+
93
+ /**
94
+ * Registers the similar_documents tool with the MCP server.
95
+ * @param server - MCP server instance
96
+ * @param client - Dimensions client instance
97
+ */
98
+ export function registerSimilarDocumentsTool(server: McpServer, client: DimensionsClient): void {
99
+ registerTrackedTool(
100
+ server,
101
+ "similar_documents",
102
+ {
103
+ description:
104
+ "Find publications or grants with similar research topics based on abstract/description text. " +
105
+ "Uses Dimensions concept extraction and weighted concepts search (not vector embeddings). " +
106
+ "Prefer this over execute_dsl for similarity. For a known record: get_by_id / get_by_doi first, " +
107
+ "then pass its abstract/description as text. Supported entityType: publications, grants only.",
108
+ inputSchema: z.object({
109
+ entityType: z
110
+ .enum(SIMILAR_DOCUMENTS_ENTITY_TYPES)
111
+ .describe("Entity type to search (publications or grants)"),
112
+ text: z
113
+ .string()
114
+ .min(1)
115
+ .describe(
116
+ "Abstract, description, or other prose to find similar documents for. " +
117
+ "Longer topical text works better than short keywords.",
118
+ ),
119
+ limit: z
120
+ .number()
121
+ .int()
122
+ .min(1)
123
+ .max(SCHEMA_LIMITS.maxLimit)
124
+ .default(20)
125
+ .describe(`Maximum results to return (max ${SCHEMA_LIMITS.maxLimit}, default 20)`),
126
+ ...PAGINATION_INPUT_SCHEMA,
127
+ fields: z
128
+ .array(z.string())
129
+ .optional()
130
+ .describe(
131
+ "Fields to return. Accepts aliases or DSL names. Use dimensions://fields/{entity} for the full list.",
132
+ ),
133
+ filters: z
134
+ .array(ExtendedWhereFilterSchema)
135
+ .optional()
136
+ .describe("Additional where-clause filters"),
137
+ yearFrom: z
138
+ .number()
139
+ .int()
140
+ .optional()
141
+ .describe("Filter from this year inclusive (publications: year; grants: start_year)"),
142
+ yearTo: z
143
+ .number()
144
+ .int()
145
+ .optional()
146
+ .describe("Filter up to this year inclusive (publications: year; grants: start_year)"),
147
+ sortBy: z
148
+ .string()
149
+ .optional()
150
+ .describe(
151
+ "Sort field (default: score for relevance). Examples: score, times_cited, year, start_year.",
152
+ ),
153
+ confirmLargeFetch: z
154
+ .boolean()
155
+ .optional()
156
+ .default(false)
157
+ .describe(
158
+ "Required when skip≥5000, page≥5, or limit=1000 with skip>0. See dimensions://schema/policy.",
159
+ ),
160
+ }),
161
+ outputSchema: z.object({
162
+ entityType: z.enum(SIMILAR_DOCUMENTS_ENTITY_TYPES).describe("Entity type searched"),
163
+ totalCount: z.number().describe("Total matching records"),
164
+ returnedCount: z.number().describe("Records returned in this response"),
165
+ truncated: z.boolean().optional().describe("True when more results exist beyond this page"),
166
+ truncationWarning: z.string().optional().describe("Warning message when truncated"),
167
+ ...PAGINATION_OUTPUT_SCHEMA,
168
+ publications: z
169
+ .array(z.record(z.string(), z.unknown()))
170
+ .optional()
171
+ .describe("Matching publications (when entityType is publications)"),
172
+ grants: z
173
+ .array(z.record(z.string(), z.unknown()))
174
+ .optional()
175
+ .describe("Matching grants (when entityType is grants)"),
176
+ }),
177
+ annotations: READ_ONLY_API_ANNOTATIONS,
178
+ },
179
+ withFieldAliases(
180
+ {
181
+ entitySource: { kind: "dynamic", argName: "entityType" },
182
+ fieldArrayArgs: ["fields"],
183
+ fieldStringArgs: ["sortBy"],
184
+ filterArrayArgs: ["filters"],
185
+ },
186
+ async (args) => {
187
+ try {
188
+ const entityType = args.entityType as SimilarDocumentsEntityType;
189
+ const record = args as Record<string, unknown>;
190
+ const { skip, limit } = resolveSkipAndLimit({
191
+ skip: record.skip as number | undefined,
192
+ page: record.page as number | undefined,
193
+ limit: (record.limit as number | undefined) ?? 20,
194
+ });
195
+
196
+ validateSearchPaginationPolicy({
197
+ skip,
198
+ limit,
199
+ confirmLargeFetch: record.confirmLargeFetch as boolean | undefined,
200
+ });
201
+
202
+ const dsl = buildSimilarDocumentsDsl(client, entityType, {
203
+ ...record,
204
+ limit,
205
+ });
206
+ const response = (await client.rawQuery(dsl)) as Record<string, unknown>;
207
+ const parsed = parseEntityResponse(response, entityType as StructuredEntityType);
208
+ const rows = parsed.data as Record<string, unknown>[];
209
+ const resultKey = searchResultKey(entityType as StructuredEntityType);
210
+
211
+ return formatToolResult(
212
+ withSearchPagination(
213
+ {
214
+ entityType,
215
+ totalCount: parsed.totalCount,
216
+ returnedCount: rows.length,
217
+ [resultKey]: rows,
218
+ },
219
+ parsed.totalCount,
220
+ rows.length,
221
+ skip,
222
+ limit,
223
+ ),
224
+ );
225
+ } catch (error) {
226
+ return formatErrorResult(error);
227
+ }
228
+ },
229
+ ),
230
+ );
231
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { randomUUID } from "node:crypto";
7
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { McpServer } from "@modelcontextprotocol/server";
8
8
  import pkg from "../../package.json" with { type: "json" };
9
9
  import {
10
10
  type McpDeploymentMode,
package/src/mcp/utils.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  * Utility functions for MCP tools.
3
3
  * @module mcp/utils
4
4
  */
5
-
6
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
5
+ import type { McpServer, ToolAnnotations } from "@modelcontextprotocol/server";
8
6
  import {
9
7
  buildPaginationMetadata,
10
8
  DimensionsError,
@@ -20,6 +20,14 @@ describe("dsl-examples", () => {
20
20
  expect(getDslExamplesForSource("unknown")).toBeUndefined();
21
21
  });
22
22
 
23
+ it("includes similar_documents examples for publications and grants", () => {
24
+ const pubs = getDslExamplesForSource("publications") ?? [];
25
+ const grants = getDslExamplesForSource("grants") ?? [];
26
+ expect(pubs.some((q) => q.includes("similar_documents("))).toBe(true);
27
+ expect(grants.some((q) => q.includes("similar_documents("))).toBe(true);
28
+ expect(DSL_GENERAL_EXAMPLES.some((q) => q.includes("similar_documents("))).toBe(true);
29
+ });
30
+
23
31
  it("uses sort by (not bare sort) and places sort before limit", () => {
24
32
  for (const dsl of ALL_EXAMPLES) {
25
33
  expect(dsl).not.toMatch(/\blimit \d+ sort\b/);
@@ -8,6 +8,7 @@ import { registerAnalyticsTools } from "../../src/mcp/tools/analytics.js";
8
8
  import { registerLookupTools } from "../../src/mcp/tools/lookup.js";
9
9
  import { registerQueryTools } from "../../src/mcp/tools/query.js";
10
10
  import { registerSearchTools } from "../../src/mcp/tools/search.js";
11
+ import { registerSimilarDocumentsTool } from "../../src/mcp/tools/similar-documents.js";
11
12
  import { testSchemaStore } from "../helpers/schema-fixture.js";
12
13
  import {
13
14
  callTool,
@@ -34,11 +35,12 @@ describe("usage-scenarios", () => {
34
35
  registerLookupTools(server as never, client as never);
35
36
  registerAnalyticsTools(server as never, client as never, schemaStore);
36
37
  registerQueryTools(server as never, client as never, schemaStore);
38
+ registerSimilarDocumentsTool(server as never, client as never);
37
39
  });
38
40
 
39
- it("documents eleven natural-language workflows", () => {
40
- expect(USAGE_SCENARIOS).toHaveLength(11);
41
- expect(new Set(USAGE_SCENARIOS.map((s) => s.id)).size).toBe(11);
41
+ it("documents thirteen natural-language workflows", () => {
42
+ expect(USAGE_SCENARIOS).toHaveLength(13);
43
+ expect(new Set(USAGE_SCENARIOS.map((s) => s.id)).size).toBe(13);
42
44
  });
43
45
 
44
46
  for (const scenario of USAGE_SCENARIOS) {
@@ -5,9 +5,7 @@
5
5
  * via in-memory transport, and executes eval suites against it.
6
6
  * @module test/integration/harness
7
7
  */
8
-
9
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
8
+ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
11
9
  import { createMcpServerAsync, type McpServerConfig } from "../../src/mcp/server.js";
12
10
  import { missingEnvVars } from "./env.js";
13
11
  import type {
@@ -2,9 +2,7 @@
2
2
  * MCP client helpers for hosted Streamable HTTP transport.
3
3
  * @module test/integration/hosted-client
4
4
  */
5
-
6
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
8
6
  import { hostedMcpEndpoint } from "./env.js";
9
7
 
10
8
  export interface HostedMcpSession {
@@ -31,7 +31,7 @@ const MOCK_HOSTED: HostedEnvConfig = {
31
31
 
32
32
  const TEST_API_KEY = "test-hosted-api-key";
33
33
 
34
- describe("Hosted MCP e2e (mocked backends)", () => {
34
+ describe("Hosted MCP e2e (mocked backends)", { timeout: 30_000 }, () => {
35
35
  let restoreFetch: (() => void) | undefined;
36
36
  let server: HostedHttpServerHandle | undefined;
37
37
  let baseUrl: string;
@@ -10,6 +10,7 @@ import { lookupSuite } from "./lookup.integration.js";
10
10
  import { querySuite } from "./query.integration.js";
11
11
  import { resourcesSuite } from "./resources.integration.js";
12
12
  import { searchSuite } from "./search.integration.js";
13
+ import { similarDocumentsSuite } from "./similar-documents.integration.js";
13
14
 
14
15
  /** All integration test suites, in execution order. */
15
16
  export const allSuites: readonly EvalSuite[] = [
@@ -17,6 +18,7 @@ export const allSuites: readonly EvalSuite[] = [
17
18
  lookupSuite,
18
19
  querySuite,
19
20
  functionsSuite,
21
+ similarDocumentsSuite,
20
22
  analyticsSuite,
21
23
  resourcesSuite,
22
24
  ];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Eval suite for similar_documents tool.
3
+ * @module test/integration/suites/similar-documents
4
+ */
5
+
6
+ import { arrayMinLength, fieldAtLeast, hasField, isSuccess } from "../assertions.js";
7
+ import type { EvalSuite } from "../types.js";
8
+
9
+ const SAMPLE_ABSTRACT =
10
+ "After spinal cord injury, macrophages infiltrate the lesion site and contribute to both tissue damage and repair processes. Understanding macrophage polarization could lead to novel therapeutic strategies.";
11
+
12
+ const SAMPLE_GRANT_TEXT =
13
+ "Development of novel cancer immunotherapy approaches using checkpoint inhibitors and CAR-T cell engineering for solid tumors.";
14
+
15
+ export const similarDocumentsSuite: EvalSuite = {
16
+ name: "similar_documents",
17
+ description: "Concept-based similar document search for publications and grants",
18
+ cases: [
19
+ {
20
+ name: "similar publications from abstract text",
21
+ tool: "similar_documents",
22
+ args: {
23
+ entityType: "publications",
24
+ text: SAMPLE_ABSTRACT,
25
+ yearFrom: 2016,
26
+ limit: 5,
27
+ fields: ["id", "title", "year"],
28
+ },
29
+ assertions: [
30
+ isSuccess(),
31
+ hasField("totalCount"),
32
+ fieldAtLeast("totalCount", 1),
33
+ arrayMinLength("publications", 1),
34
+ ],
35
+ timeout: 60_000,
36
+ },
37
+ {
38
+ name: "similar grants from description text",
39
+ tool: "similar_documents",
40
+ args: {
41
+ entityType: "grants",
42
+ text: SAMPLE_GRANT_TEXT,
43
+ yearFrom: 2020,
44
+ limit: 5,
45
+ fields: ["id", "title"],
46
+ },
47
+ assertions: [isSuccess(), hasField("totalCount"), arrayMinLength("grants", 1)],
48
+ timeout: 60_000,
49
+ },
50
+ ],
51
+ };
@@ -3,9 +3,7 @@
3
3
  * Live verification of docs/USAGE.md natural-language scenarios.
4
4
  * Run: node --import tsx apps/mcp/test/integration/verify-usage-scenarios.ts
5
5
  */
6
-
7
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
+ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
9
7
  import { USAGE_SCENARIOS } from "../../src/mcp/examples/usage-scenarios.js";
10
8
  import { buildServerInstructions, createMcpServerAsync } from "../../src/mcp/server.js";
11
9