@equinor/fusion-framework-cli-plugin-ai-mcp 1.0.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 (38) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +131 -0
  4. package/dist/esm/index.js +11 -0
  5. package/dist/esm/index.js.map +1 -0
  6. package/dist/esm/mcp.js +66 -0
  7. package/dist/esm/mcp.js.map +1 -0
  8. package/dist/esm/tools/fusion-search-api.js +65 -0
  9. package/dist/esm/tools/fusion-search-api.js.map +1 -0
  10. package/dist/esm/tools/fusion-search-cookbook.js +65 -0
  11. package/dist/esm/tools/fusion-search-cookbook.js.map +1 -0
  12. package/dist/esm/tools/fusion-search-eds.js +65 -0
  13. package/dist/esm/tools/fusion-search-eds.js.map +1 -0
  14. package/dist/esm/tools/fusion-search-markdown.js +65 -0
  15. package/dist/esm/tools/fusion-search-markdown.js.map +1 -0
  16. package/dist/esm/tools/fusion-search.tool.js +152 -0
  17. package/dist/esm/tools/fusion-search.tool.js.map +1 -0
  18. package/dist/esm/version.js +3 -0
  19. package/dist/esm/version.js.map +1 -0
  20. package/dist/tsconfig.tsbuildinfo +1 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/mcp.d.ts +2 -0
  23. package/dist/types/tools/fusion-search-api.d.ts +37 -0
  24. package/dist/types/tools/fusion-search-cookbook.d.ts +37 -0
  25. package/dist/types/tools/fusion-search-eds.d.ts +37 -0
  26. package/dist/types/tools/fusion-search-markdown.d.ts +37 -0
  27. package/dist/types/tools/fusion-search.tool.d.ts +52 -0
  28. package/dist/types/version.d.ts +1 -0
  29. package/package.json +61 -0
  30. package/src/index.ts +13 -0
  31. package/src/mcp.ts +130 -0
  32. package/src/tools/fusion-search-api.ts +90 -0
  33. package/src/tools/fusion-search-cookbook.ts +90 -0
  34. package/src/tools/fusion-search-eds.ts +91 -0
  35. package/src/tools/fusion-search-markdown.ts +90 -0
  36. package/src/tools/fusion-search.tool.ts +187 -0
  37. package/src/version.ts +2 -0
  38. package/tsconfig.json +24 -0
@@ -0,0 +1,90 @@
1
+ import type { FrameworkInstance } from '@equinor/fusion-framework-cli-plugin-ai-base';
2
+ import type { AiOptions } from '@equinor/fusion-framework-cli-plugin-ai-base/command-options';
3
+
4
+ /**
5
+ * Tool definition for searching Fusion Framework markdown documentation.
6
+ * This tool enables semantic search across markdown files, guides, and documentation.
7
+ */
8
+ export const toolDefinition = {
9
+ name: 'fusion_search_markdown',
10
+ description:
11
+ 'Search the Fusion Framework markdown documentation using semantic search. Use this for finding documentation, guides, and explanatory content.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ query: {
16
+ type: 'string',
17
+ description: 'The search query to find relevant markdown documentation',
18
+ },
19
+ limit: {
20
+ type: 'number',
21
+ description: 'Maximum number of results to return (default: 5)',
22
+ default: 5,
23
+ },
24
+ },
25
+ required: ['query'],
26
+ },
27
+ } as const;
28
+
29
+ /**
30
+ * Tool handler for fusion_search_markdown
31
+ */
32
+ export async function handleTool(
33
+ args: Record<string, unknown>,
34
+ framework: FrameworkInstance,
35
+ options: AiOptions & { verbose?: boolean },
36
+ ): Promise<{
37
+ content: Array<{ type: 'text'; text: string }>;
38
+ isError?: boolean;
39
+ }> {
40
+ if (!options.azureSearchIndexName || !framework.ai) {
41
+ throw new Error(
42
+ 'Vector store is not configured. Azure Search is required for search functionality.',
43
+ );
44
+ }
45
+
46
+ const query = args?.query as string;
47
+ const limit = (args?.limit as number) || 5;
48
+
49
+ if (!query || typeof query !== 'string') {
50
+ throw new Error('Query parameter is required and must be a string');
51
+ }
52
+
53
+ if (options.verbose) {
54
+ console.error(`🔍 Searching markdown documentation for: ${query}`);
55
+ }
56
+
57
+ const vectorStoreService = framework.ai.getService('search', options.azureSearchIndexName);
58
+ const retriever = vectorStoreService.asRetriever({
59
+ k: limit,
60
+ searchType: 'similarity',
61
+ filter: {
62
+ filterExpression: "metadata/attributes/any(x: x/key eq 'type' and x/value eq 'markdown')",
63
+ },
64
+ });
65
+
66
+ const docs = await retriever.invoke(query);
67
+
68
+ return {
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: JSON.stringify(
73
+ {
74
+ query,
75
+ type: 'markdown',
76
+ results: docs.map(
77
+ (doc: { pageContent: string; metadata: Record<string, unknown> }) => ({
78
+ content: doc.pageContent,
79
+ metadata: doc.metadata,
80
+ }),
81
+ ),
82
+ count: docs.length,
83
+ },
84
+ null,
85
+ 2,
86
+ ),
87
+ },
88
+ ],
89
+ };
90
+ }
@@ -0,0 +1,187 @@
1
+ import { z } from 'zod';
2
+ import type { FrameworkInstance } from '@equinor/fusion-framework-cli-plugin-ai-base';
3
+ import type { AiOptions } from '@equinor/fusion-framework-cli-plugin-ai-base/command-options';
4
+ import type { VectorStoreDocument } from '@equinor/fusion-framework-module-ai/lib';
5
+ import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
+
8
+ /** Valid documentation categories that can be searched */
9
+ export type FusionSearchCategory = 'api' | 'cookbook' | 'markdown' | 'eds' | 'all';
10
+
11
+ export const inputSchema = z.object({
12
+ query: z
13
+ .string()
14
+ .describe(
15
+ 'Ask anything about Fusion Framework in plain English — the more specific, the better the results.',
16
+ ),
17
+ limit: z
18
+ .number()
19
+ .optional()
20
+ .default(5)
21
+ .describe(
22
+ 'Maximum number of results to return. Recommended range: 3–8 (default: 5). Lower values help prevent context window overflow.',
23
+ ),
24
+ category: z
25
+ // .enum(['api', 'cookbook', 'markdown', 'eds', 'all'])
26
+ .enum(['all'])
27
+ .optional()
28
+ .default('all'),
29
+ // .describe(
30
+ // [
31
+ // 'Optional filter to narrow search scope:',
32
+ // '• api → TypeScript API reference (classes, methods, interfaces)',
33
+ // '• cookbook → Code examples, tutorials, and practical recipes',
34
+ // '• markdown → Guides, architecture docs, migration guides',
35
+ // '• eds → EDS components (Storybook stories, props, tokens, accessibility)',
36
+ // '• all → Search all sources (default)',
37
+ // ].join('\n'),
38
+ // ),
39
+ });
40
+
41
+ /**
42
+ * Tool configuration for McpServer.registerTool().
43
+ */
44
+ export const toolConfig = {
45
+ description: [
46
+ 'THE BEST AND FASTEST WAY to get accurate answers about Fusion Framework, modules, configurators, EDS components, and best practices.',
47
+ 'Understands natural questions and returns the exact code examples, API docs, cookbooks, or EDS stories you need — always with direct GitHub links.',
48
+ 'Covers everything: TypeScript APIs, practical cookbooks, architecture guides, and full EDS component documentation.',
49
+ ].join('\n'),
50
+ inputSchema,
51
+ } as const;
52
+
53
+ /**
54
+ * Azure Cognitive Search OData filter expressions per category
55
+ */
56
+ const FILTER_EXPRESSIONS: Readonly<Record<FusionSearchCategory, string | undefined>> = {
57
+ api: "metadata/attributes/any(x: x/key eq 'type' and x/value eq 'tsdoc')",
58
+ cookbook: "metadata/attributes/any(x: x/key eq 'type' and x/value eq 'cookbook')",
59
+ markdown: "metadata/attributes/any(x: x/key eq 'type' and x/value eq 'markdown')",
60
+ eds: "metadata/attributes/any(x: x/key eq 'type' and x/value eq 'storybook')",
61
+ all: undefined,
62
+ } as const;
63
+
64
+ /**
65
+ * Performs a semantic search with Maximal Marginal Relevance (MMR) in a specific category.
66
+ *
67
+ * MMR balances relevance and diversity — perfect for avoiding near-duplicate results
68
+ * when multiple docs describe the same concept.
69
+ *
70
+ * @param category - Documentation category to search within
71
+ * @param query - Natural language search query
72
+ * @param limit - Maximum number of results (final count after MMR)
73
+ * @param framework - Fusion Framework instance with AI module loaded
74
+ * @param options - AI plugin options (must include Azure Search index name)
75
+ * @returns Array of relevant documents with metadata
76
+ */
77
+ async function searchWithMMR(
78
+ category: FusionSearchCategory,
79
+ query: string,
80
+ limit: number,
81
+ framework: FrameworkInstance,
82
+ options: AiOptions,
83
+ ): Promise<VectorStoreDocument[]> {
84
+ const filterExpression = FILTER_EXPRESSIONS[category];
85
+
86
+ if (!framework.ai || !options.azureSearchIndexName) {
87
+ throw new Error(
88
+ 'Fusion AI module or Azure Search index not configured. ' +
89
+ 'Ensure `@equinor/fusion-framework-cli-plugin-ai` is installed and configured.',
90
+ );
91
+ }
92
+
93
+ const vectorStoreService = framework.ai.getService('search', options.azureSearchIndexName);
94
+
95
+ const retriever = vectorStoreService.asRetriever({
96
+ k: limit,
97
+ searchType: 'mmr',
98
+ searchKwargs: {
99
+ fetchK: Math.max(60, limit * 6), // Fetch more candidates for better diversity
100
+ lambda: 0.5, // Balance between relevance (1.0) and diversity (0.0)
101
+ },
102
+ filter: filterExpression ? { filterExpression } : undefined,
103
+ });
104
+
105
+ const docs = await retriever.invoke(query);
106
+ return Array.isArray(docs) ? (docs as VectorStoreDocument[]) : [];
107
+ }
108
+
109
+ /**
110
+ * Main handler for the `fusion_search` tool.
111
+ *
112
+ * Returns a curried function that takes tool input arguments and returns search results.
113
+ *
114
+ * @param framework - Active Fusion Framework instance
115
+ * @param options - AI plugin options, optionally with `{ verbose: true }`
116
+ * @returns A function that takes validated tool arguments and returns search results
117
+ *
118
+ * @throws {Error} If AI module or Azure Search index is not configured
119
+ */
120
+ export function handleTool(
121
+ framework: FrameworkInstance,
122
+ options: AiOptions & { verbose?: boolean },
123
+ ): ToolCallback<typeof inputSchema> {
124
+ return async (args: Parameters<ToolCallback<typeof inputSchema>>[0]): Promise<CallToolResult> => {
125
+ // ── Preconditions ─────────────────────────────────────────────────────
126
+ if (!framework.ai || !options.azureSearchIndexName) {
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text' as const,
131
+ text: JSON.stringify({
132
+ error: 'Fusion AI module or Azure Search index not configured',
133
+ message:
134
+ 'Ensure `@equinor/fusion-framework-cli-plugin-ai` is installed and configured with Azure Search credentials.',
135
+ required: {
136
+ azureSearchIndexName: 'Azure Search index name',
137
+ azureSearchEndpoint: 'Azure Search endpoint URL',
138
+ azureSearchApiKey: 'Azure Search API key',
139
+ openaiEmbeddingDeployment: 'Azure OpenAI embedding deployment name',
140
+ },
141
+ }),
142
+ },
143
+ ],
144
+ isError: true,
145
+ };
146
+ }
147
+ const query = args.query.trim();
148
+ const limit = args.limit && args.limit > 0 ? Math.floor(args.limit) : 10;
149
+ const category: FusionSearchCategory =
150
+ args.category && args.category in FILTER_EXPRESSIONS ? args.category : 'all';
151
+
152
+ if (!query || query === '') {
153
+ throw new Error('Parameter "query" is required and must be a non-empty string.');
154
+ }
155
+
156
+ // ── Verbose logging (useful when running in Cursor, VS Code, etc.) ───
157
+ if (options.verbose) {
158
+ console.error(`Fusion Search → "${query}" | Category: ${category} | Limit: ${limit}`);
159
+ }
160
+
161
+ // ── Execute search ───────────────────────────────────────────────────
162
+ try {
163
+ const results = await searchWithMMR(category, query, limit, framework, options);
164
+ return {
165
+ content: results.map((doc) => ({
166
+ type: 'text' as const,
167
+ text: doc.pageContent,
168
+ _meta: doc.metadata,
169
+ })),
170
+ isError: false,
171
+ };
172
+ } catch (error) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text' as const,
177
+ text: JSON.stringify({
178
+ error: error instanceof Error ? error.message : String(error),
179
+ results: [],
180
+ }),
181
+ },
182
+ ],
183
+ isError: true,
184
+ };
185
+ }
186
+ };
187
+ }
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Generated by genversion.
2
+ export const version = '1.0.0';
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist/esm",
7
+ "rootDir": "src",
8
+ "declarationDir": "./dist/types",
9
+ "baseUrl": "."
10
+ },
11
+ "references": [
12
+ {
13
+ "path": "../ai-base"
14
+ },
15
+ {
16
+ "path": "../../modules/ai"
17
+ },
18
+ {
19
+ "path": "../../modules/module"
20
+ }
21
+ ],
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules"]
24
+ }