@moorchehai/mcp 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,6 +208,7 @@ The Moorcheh MCP server provides tools for:
208
208
  ### Data Tools
209
209
  - **`upload-text`**: Upload text documents to a namespace
210
210
  - **`upload-vectors`**: Upload vector embeddings to a namespace
211
+ - **`upload-file`**: Upload files directly to a text-type namespace (supports .pdf, .docx, .xlsx, .json, .txt, .csv, .md, max 10MB)
211
212
  - **`get-data`**: Retrieve text documents by ID from text namespaces
212
213
  - **`delete-data`**: Remove specific data items from a namespace
213
214
 
@@ -219,11 +220,14 @@ The Moorcheh MCP server provides tools for:
219
220
 
220
221
  | Model ID | Name | Provider | Description |
221
222
  |----------|------|----------|-------------|
222
- | `anthropic.claude-3-7-sonnet-20250219-v1:0` | Claude 3.7 Sonnet | Anthropic | Latest Claude model with enhanced capabilities |
223
- | `anthropic.claude-sonnet-4-20250514-v1:0` | Claude Sonnet 4 | Anthropic | Latest Claude model with enhanced capabilities |
224
- | `meta.llama4-maverick-17b-instruct-v1:0` | Llama 4 Maverick | Meta | Latest Llama model optimized for instruction following |
225
- | `meta.llama3-3-70b-instruct-v1:0` | Llama 3 70B | Meta | Large Llama model with strong general capabilities |
226
- | `deepseek.r1-v1:0` | DeepSeek-R1 | DeepSeek | Specialized model for research and analysis |
223
+ | `anthropic.claude-sonnet-4-20250514-v1:0` | Claude Sonnet 4 | Anthropic | Hybrid reasoning, extended thinking, efficient code generation |
224
+ | `anthropic.claude-sonnet-4-5-20250929-v1:0` | Claude Sonnet 4.5 | Anthropic | Latest Claude model with enhanced capabilities and agentic search |
225
+ | `meta.llama4-maverick-17b-instruct-v1:0` | Llama 4 Maverick 17B | Meta | 1M token context, fine tuning, text summarization, function calling |
226
+ | `meta.llama3-3-70b-instruct-v1:0` | Llama 3.3 70B | Meta | Advanced reasoning and decision making capabilities |
227
+ | `amazon.nova-pro-v1:0` | Amazon Nova Pro | Amazon | 300K context, chat optimized, complex reasoning, math |
228
+ | `deepseek.r1-v1:0` | DeepSeek R1 | DeepSeek | Advanced reasoning and code generation |
229
+ | `openai.gpt-oss-120b-1:0` | OpenAI GPT OSS 120B | OpenAI | Hybrid reasoning, extended thinking, efficient research |
230
+ | `qwen.qwen3-32b-v1:0` | Qwen 3 32B | Qwen | Text generation and code generation |
227
231
 
228
232
  ## Prerequisites
229
233
 
@@ -289,34 +293,4 @@ We welcome contributions! Please feel free to submit a Pull Request.
289
293
 
290
294
  ## Changelog
291
295
 
292
- ### v1.2.2
293
- - Package Name: Updated to `@moorchehai/mcp` for official Moorcheh organization
294
- - NPX Support: Added CLI wrapper for seamless `npx -y @moorchehai/mcp` execution
295
- - Package Structure: Configured for npm registry publishing
296
- - CLI Features: Added help, version commands and API key validation
297
- - User Experience: Improved error messages and installation guidance
298
-
299
- ### v1.2.1
300
- - NPX Support: Added CLI wrapper for seamless `npx -y @moorcheh/mcp` execution
301
- - Package Structure: Configured for npm registry publishing as `@moorcheh/mcp`
302
- - CLI Features: Added help, version commands and API key validation
303
- - User Experience: Improved error messages and installation guidance
304
-
305
- ### v1.2.0
306
- - New tool: `get-data` to fetch documents by ID from text namespaces (POST /namespaces/{name}/documents/get)
307
- - Reliability: Static documentation resources to avoid invalid URI errors in MCP clients
308
- - Windows compatibility: Use ';' for command chaining in PowerShell
309
- - Stability: Ensured stdout handling respects MCP JSON-RPC framing
310
-
311
- ### v1.1.0
312
- - Enhanced prompt system with dynamic content generation
313
- - Added comprehensive argument schemas with Zod validation
314
- - Improved search optimization, data organization, and AI answer setup prompts
315
- - Updated prompt registration to use new MCP SDK signature
316
- - Better user guidance and interactive prompt responses
317
-
318
- ### v1.0.0
319
- - Initial release with MCP server functionality
320
- - Support for text and vector operations
321
- - AI-powered answer generation
322
- - Comprehensive documentation
296
+ For a detailed list of changes, see [CHANGELOG.md](CHANGELOG.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moorchehai/mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Moorcheh MCP Server with completable functionality for AI-powered search and answer operations",
5
5
  "main": "src/server/index.js",
6
6
  "type": "module",
@@ -36,9 +36,10 @@
36
36
  "author": "Moorcheh Team",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.17.0",
40
- "zod": "^3.22.4",
41
- "axios": "^1.11.0"
39
+ "@modelcontextprotocol/sdk": "^1.25.2",
40
+ "axios": "^1.12.0",
41
+ "form-data": "^4.0.5",
42
+ "zod": "^3.22.4"
42
43
  },
43
44
  "devDependencies": {
44
45
  "node": ">=18.0.0"
@@ -1,7 +1,8 @@
1
1
  import axios from 'axios';
2
- import { readFileSync } from 'fs';
2
+ import { readFileSync, createReadStream, statSync } from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
- import { dirname, join } from 'path';
4
+ import { dirname, join, basename } from 'path';
5
+ import FormData from 'form-data';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -87,4 +88,57 @@ async function makeApiRequest(method, url, data = null) {
87
88
  }
88
89
  }
89
90
 
90
- export { API_ENDPOINTS, makeApiRequest, MOORCHEH_API_KEY };
91
+ // Helper function to upload files (multipart/form-data)
92
+ async function uploadFile(namespace_name, filePath) {
93
+ try {
94
+ // Check if file exists
95
+ const stats = statSync(filePath);
96
+ const fileSizeInMB = stats.size / (1024 * 1024);
97
+
98
+ // Check file size (max 10MB)
99
+ if (fileSizeInMB > 10) {
100
+ throw new Error(`File size (${fileSizeInMB.toFixed(2)}MB) exceeds maximum allowed size of 10MB`);
101
+ }
102
+
103
+ // Check file extension
104
+ const allowedExtensions = ['.pdf', '.docx', '.xlsx', '.json', '.txt', '.csv', '.md'];
105
+ const fileExtension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
106
+ if (!allowedExtensions.includes(fileExtension)) {
107
+ throw new Error(`File type '${fileExtension}' is not supported. Allowed types: ${allowedExtensions.join(', ')}`);
108
+ }
109
+
110
+ // Create FormData
111
+ const formData = new FormData();
112
+ const fileName = basename(filePath);
113
+ formData.append('file', createReadStream(filePath), fileName);
114
+
115
+ // Make the request
116
+ const url = `${API_ENDPOINTS.namespaces}/${namespace_name}/upload-file`;
117
+ const response = await axios.post(url, formData, {
118
+ headers: {
119
+ 'x-api-key': MOORCHEH_API_KEY,
120
+ ...formData.getHeaders(),
121
+ },
122
+ maxContentLength: Infinity,
123
+ maxBodyLength: Infinity,
124
+ });
125
+
126
+ return response.data;
127
+ } catch (error) {
128
+ if (error.response) {
129
+ const status = error.response.status;
130
+ const data = error.response.data;
131
+
132
+ if (status === 403) {
133
+ throw new Error(`Forbidden: Check your API key. Status: ${status}, Response: ${JSON.stringify(data)}`);
134
+ } else if (status === 401) {
135
+ throw new Error(`Unauthorized: Invalid API key. Status: ${status}, Response: ${JSON.stringify(data)}`);
136
+ } else {
137
+ throw new Error(`API Error (${status}): ${JSON.stringify(data)}`);
138
+ }
139
+ }
140
+ throw new Error(`File upload error: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ export { API_ENDPOINTS, makeApiRequest, uploadFile, MOORCHEH_API_KEY };
@@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
5
 
6
6
  // Import tools
7
7
  import { listNamespacesTool, createNamespaceTool, deleteNamespaceTool } from './tools/namespace-tools.js';
8
- import { uploadTextTool, uploadVectorsTool, deleteDataTool, getDataTool } from './tools/data-tools.js';
8
+ import { uploadTextTool, uploadVectorsTool, deleteDataTool, getDataTool, uploadFileTool } from './tools/data-tools.js';
9
9
  import { searchTool, answerTool } from './tools/search-tools.js';
10
10
 
11
11
  // Import resources
@@ -156,6 +156,13 @@ server.tool(
156
156
  getDataTool.handler,
157
157
  );
158
158
 
159
+ server.tool(
160
+ uploadFileTool.name,
161
+ uploadFileTool.description,
162
+ uploadFileTool.parameters,
163
+ uploadFileTool.handler,
164
+ );
165
+
159
166
  // Register search tools
160
167
  server.tool(
161
168
  searchTool.name,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
2
+ import { makeApiRequest, API_ENDPOINTS, uploadFile } from '../config/api.js';
3
3
 
4
4
  // Upload text documents tool
5
5
  export const uploadTextTool = {
@@ -155,4 +155,39 @@ export const getDataTool = {
155
155
  };
156
156
  }
157
157
  },
158
+ };
159
+
160
+ // Upload file tool
161
+ export const uploadFileTool = {
162
+ name: "upload-file",
163
+ description: "Upload a file directly to a text-type namespace for processing and indexing. Files are queued for ingestion and will be available for search once processed. Supported file types: .pdf, .docx, .xlsx, .json, .txt, .csv, .md (max 10MB)",
164
+ parameters: {
165
+ namespace_name: z.string().describe("Name of the text namespace to upload the file to"),
166
+ file_path: z.string().describe("Path to the file to upload (max 10MB). Must be one of: .pdf, .docx, .xlsx, .json, .txt, .csv, .md"),
167
+ },
168
+ handler: async ({ namespace_name, file_path }) => {
169
+ try {
170
+ const data = await uploadFile(namespace_name, file_path);
171
+
172
+ const resultText = `Successfully uploaded file "${data.fileName}" to namespace "${namespace_name}":\n${JSON.stringify(data, null, 2)}`;
173
+
174
+ return {
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: resultText,
179
+ },
180
+ ],
181
+ };
182
+ } catch (error) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `Error uploading file: ${error.message}`,
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ },
158
193
  };
@@ -1,230 +1,274 @@
1
- import { z } from 'zod';
2
- import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
3
-
4
- // Search tool
5
- export const searchTool = {
6
- name: "search",
7
- description: "Search for data in a namespace using semantic search or vector similarity. This tool provides powerful search capabilities across your namespaces, supporting both text-based semantic search and vector-based similarity search. For text search, you can use natural language queries to find relevant documents based on meaning rather than just keywords. For vector search, you can find similar content by comparing vector embeddings. The tool supports advanced features like result filtering, similarity thresholds, and kiosk mode for production environments. This is ideal for building intelligent search interfaces, recommendation systems, or content discovery features.",
8
- parameters: {
9
- namespaces: z.array(z.string().min(1)).min(1).describe("Namespaces to search in. Provide an array of namespace names where you want to search for content. You can search across multiple namespaces simultaneously. All namespaces must be accessible with your API key."),
10
- query: z.union([z.string().min(1), z.array(z.number())]).describe("Search query. For text search: provide a natural language query string (e.g., 'tell me about the company?', 'how to configure authentication?'). For vector search: provide an array of numbers representing a vector embedding (e.g., [0.1, 0.2, 0.3, ..., 0.768]). The query type will be automatically detected based on the input format. DO NOT USE QUOTES IN THE QUERY FOR VECTOR SEARCH."),
11
- query_type: z.enum(['text', 'vector']).optional().describe("Type of query to perform. 'text' for semantic search using natural language queries. 'vector' for similarity search using vector embeddings. If not specified, the type will be automatically detected based on the query format (string for text, array for vector)."),
12
- top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many search results are returned, with higher values providing more comprehensive results. Default is 10. Use lower values (3-5) for focused results, higher values (10-20) for broader exploration."),
13
- threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters results based on similarity score. Higher values (0.7-0.9) return only highly similar results, lower values (0.3-0.5) return more comprehensive results. Required when kiosk_mode is true."),
14
- kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled results suitable for production environments. When false, search across all specified namespaces without strict filtering."),
15
- },
16
- handler: async ({ namespaces, query, query_type, top_k = 10, threshold, kiosk_mode = false }) => {
17
- try {
18
- // Determine query type if not explicitly provided
19
- let finalQueryType = query_type;
20
- let finalQuery = query;
21
-
22
- if (!finalQueryType) {
23
- if (typeof query === 'string') {
24
- // Check if it's a string representation of a vector array
25
- if (query.startsWith('[') && query.endsWith(']')) {
26
- try {
27
- const parsedArray = JSON.parse(query);
28
- if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
29
- finalQuery = parsedArray;
30
- finalQueryType = 'vector';
31
- } else {
32
- finalQueryType = 'text';
33
- }
34
- } catch (e) {
35
- finalQueryType = 'text';
36
- }
37
- } else {
38
- finalQueryType = 'text';
39
- }
40
- } else if (Array.isArray(query) && query.every(item => typeof item === 'number')) {
41
- finalQueryType = 'vector';
42
- } else {
43
- return {
44
- content: [
45
- {
46
- type: "text",
47
- text: 'Error: Unable to determine query type. Please specify query_type parameter or provide a valid string (for text) or number array (for vector).',
48
- },
49
- ],
50
- };
51
- }
52
- }
53
-
54
- // Handle vector query type with string input
55
- if (finalQueryType === 'vector' && typeof query === 'string') {
56
- try {
57
- const parsedArray = JSON.parse(query);
58
- if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
59
- finalQuery = parsedArray;
60
- } else {
61
- return {
62
- content: [
63
- {
64
- type: "text",
65
- text: 'Error: Vector query type requires an array of numbers',
66
- },
67
- ],
68
- };
69
- }
70
- } catch (e) {
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: 'Error: Vector query type requires an array of numbers',
76
- },
77
- ],
78
- };
79
- }
80
- }
81
-
82
- // Validate query format matches query type
83
- if (finalQueryType === 'text' && typeof finalQuery !== 'string') {
84
- return {
85
- content: [
86
- {
87
- type: "text",
88
- text: 'Error: Text query type requires a string query. Example: "your search text here"',
89
- },
90
- ],
91
- };
92
- }
93
- if (finalQueryType === 'vector' && (!Array.isArray(finalQuery) || !finalQuery.every(item => typeof item === 'number'))) {
94
- return {
95
- content: [
96
- {
97
- type: "text",
98
- text: 'Error: Vector query type requires an array of numbers. Example: [0.1, 0.2, 0.3, 0.4, 0.5] for 5-dimensional namespace',
99
- },
100
- ],
101
- };
102
- }
103
-
104
- const requestBody = {
105
- namespaces,
106
- query: finalQuery,
107
- top_k,
108
- kiosk_mode,
109
- };
110
- if (threshold !== undefined) {
111
- requestBody.threshold = threshold;
112
- }
113
-
114
- const data = await makeApiRequest('POST', API_ENDPOINTS.search, requestBody);
115
-
116
- if (!data.results || data.results.length === 0) {
117
- return {
118
- content: [
119
- {
120
- type: "text",
121
- text: `No results found for query: "${query}"`,
122
- },
123
- ],
124
- };
125
- }
126
-
127
- const formattedResults = data.results.map((result, index) =>
128
- [
129
- `Result ${index + 1}:`,
130
- `Content: ${result.content}`,
131
- `Score: ${result.score || 'N/A'}`,
132
- `Metadata: ${JSON.stringify(result.metadata || {}, null, 2)}`,
133
- "---",
134
- ].join("\n")
135
- );
136
-
137
- const searchText = `Search results for "${query}" in namespaces [${namespaces.join(', ')}]:\n\n${formattedResults.join("\n")}\n\nTotal results: ${data.total || data.results.length}`;
138
-
139
- return {
140
- content: [
141
- {
142
- type: "text",
143
- text: searchText,
144
- },
145
- ],
146
- };
147
- } catch (error) {
148
- return {
149
- content: [
150
- {
151
- type: "text",
152
- text: `Error searching: ${error.message}`,
153
- },
154
- ],
155
- };
156
- }
157
- },
158
- };
159
-
160
- // Answer tool
161
- export const answerTool = {
162
- name: "answer",
163
- description: "Get AI-generated answers based on data in a namespace using text queries. This tool provides intelligent, context-aware responses by searching through your stored text documents and generating comprehensive answers using advanced language models.",
164
- parameters: {
165
- namespace: z.string().min(1).describe("Namespace to answer questions from. Must be a text namespace containing the documents that will be searched to generate answers. The namespace should contain relevant content that can answer the types of questions you expect to ask."),
166
- query: z.string().min(1).describe("Text query for AI answer generation. Provide a natural language question or prompt that you want the AI to answer. The AI will search through your namespace content and generate a comprehensive response based on the relevant information found."),
167
- top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many relevant documents the AI considers when generating an answer. Default is 5. Use lower values (3-5) for focused answers, higher values (8-10) for comprehensive responses that consider more context."),
168
- threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters documents based on relevance before generating the answer. Higher values (0.7-0.9) ensure only highly relevant content is used, lower values (0.3-0.5) include more context. Required when kiosk_mode is true."),
169
- kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled and focused answers suitable for production environments."),
170
- aiModel: z.string().optional().describe("AI model to use for answer generation. Different models may have different capabilities, response styles, and performance characteristics. Supported AI models include: 'anthropic.claude-3-7-sonnet-20250219-v1:0' (Claude 3.7 Sonnet), 'anthropic.claude-sonnet-4-20250514-v1:0' (Claude Sonnet 4), 'meta.llama4-maverick-17b-instruct-v1:0' (Llama 4 Maverick), 'meta.llama3-3-70b-instruct-v1:0' (Llama 3.3 70B), 'deepseek.r1-v1:0' (DeepSeek R1). If not specified, defaults to Claude 3.7 Sonnet."),
171
- chatHistory: z.array(z.object({
172
- role: z.string().describe("Role of the message in the conversation. Use 'user' for user messages and 'assistant' for AI responses. This helps maintain conversation context and allows the AI to reference previous exchanges."),
173
- content: z.string()
174
- })).optional().describe("Chat history for AI answer generation. Provide previous conversation context to help the AI maintain continuity and reference earlier parts of the conversation. This enables more coherent multi-turn conversations."),
175
- headerPrompt: z.string().optional().describe("Header prompt for AI answer generation. Custom instructions that define the AI's role, style, and behavior. Use this to create specialized assistants (e.g., technical support, friendly helper, formal advisor) or set specific guidelines for response generation."),
176
- footerPrompt: z.string().optional().describe("Footer prompt for AI answer generation. Additional instructions that are applied after the main response generation. Useful for formatting requirements, citation styles, or specific response patterns that should be consistently applied."),
177
- temperature: z.number().min(0).max(2.0).optional().describe("Temperature for AI answer generation. Controls the creativity and randomness of responses. Lower values (0.1-0.3) produce more focused, deterministic answers. Higher values (0.7-1.0) produce more creative, varied responses. Default is 0.7."),
178
- },
179
- handler: async ({ namespace, query, top_k = 5, threshold, kiosk_mode = false, aiModel, chatHistory = [], headerPrompt, footerPrompt, temperature = 0.7 }) => {
180
- try {
181
- const requestBody = {
182
- namespace,
183
- query,
184
- top_k,
185
- kiosk_mode,
186
- };
187
-
188
- if (threshold !== undefined) {
189
- requestBody.threshold = threshold;
190
- }
191
- if (aiModel) {
192
- requestBody.aiModel = aiModel;
193
- }
194
- if (chatHistory && chatHistory.length > 0) {
195
- requestBody.chatHistory = chatHistory;
196
- }
197
- if (headerPrompt) {
198
- requestBody.headerPrompt = headerPrompt;
199
- }
200
- if (footerPrompt) {
201
- requestBody.footerPrompt = footerPrompt;
202
- }
203
- if (temperature !== undefined) {
204
- requestBody.temperature = temperature;
205
- }
206
-
207
- const data = await makeApiRequest('POST', API_ENDPOINTS.answer, requestBody);
208
-
209
- const resultText = `AI Answer for "${query}" in namespace "${namespace}":\n\n${data.answer || data.response || JSON.stringify(data, null, 2)}`;
210
-
211
- return {
212
- content: [
213
- {
214
- type: "text",
215
- text: resultText,
216
- },
217
- ],
218
- };
219
- } catch (error) {
220
- return {
221
- content: [
222
- {
223
- type: "text",
224
- text: `Error getting AI answer: ${error.message}`,
225
- },
226
- ],
227
- };
228
- }
229
- },
1
+ import { z } from 'zod';
2
+ import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
3
+
4
+ // Search tool
5
+ export const searchTool = {
6
+ name: "search",
7
+ description: "Search for data in a namespace using semantic search or vector similarity. This tool provides powerful search capabilities across your namespaces, supporting both text-based semantic search and vector-based similarity search. For text search, you can use natural language queries to find relevant documents based on meaning rather than just keywords. For vector search, you can find similar content by comparing vector embeddings. The tool supports advanced features like result filtering, similarity thresholds, metadata filters, keyword filters, and kiosk mode for production environments. This is ideal for building intelligent search interfaces, recommendation systems, or content discovery features.\n\nFiltering Capabilities:\n- Metadata Filters: Use #key:value format (e.g., #category:tech, #priority:high)\n- Keyword Filters: Use #keyword format (e.g., #important, #urgent)\n- Filters only apply to text search and metadata must be manually uploaded with documents",
8
+ parameters: {
9
+ namespaces: z.array(z.string().min(1)).min(1).describe("Namespaces to search in. Provide an array of namespace names where you want to search for content. You can search across multiple namespaces simultaneously. All namespaces must be accessible with your API key."),
10
+ query: z.union([z.string().min(1), z.array(z.number())]).describe("Search query. For text search: provide a natural language query string (e.g., 'tell me about the company?', 'how to configure authentication?'). For vector search: provide an array of numbers representing a vector embedding (e.g., [0.1, 0.2, 0.3, ..., 0.768]). The query type will be automatically detected based on the input format. DO NOT USE QUOTES IN THE QUERY FOR VECTOR SEARCH.\n\nFiltering: For text search, you can include filters in your query:\n- Metadata filters: #category:tech #priority:high\n- Keyword filters: #important #urgent\n- Combine both: 'serverless benefits #category:tech #important'"),
11
+ query_type: z.enum(['text', 'vector']).optional().describe("Type of query to perform. 'text' for semantic search using natural language queries. 'vector' for similarity search using vector embeddings. If not specified, the type will be automatically detected based on the query format (string for text, array for vector)."),
12
+ top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many search results are returned, with higher values providing more comprehensive results. Default is 10. Use lower values (3-5) for focused results, higher values (10-20) for broader exploration."),
13
+ threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters results based on similarity score. Higher values (0.7-0.9) return only highly similar results, lower values (0.3-0.5) return more comprehensive results. Required when kiosk_mode is true."),
14
+ kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled results suitable for production environments. When false, search across all specified namespaces without strict filtering."),
15
+ },
16
+ handler: async ({ namespaces, query, query_type, top_k = 10, threshold, kiosk_mode = false }) => {
17
+ try {
18
+ // Determine query type if not explicitly provided
19
+ let finalQueryType = query_type;
20
+ let finalQuery = query;
21
+
22
+ if (!finalQueryType) {
23
+ if (typeof query === 'string') {
24
+ // Check if it's a string representation of a vector array
25
+ if (query.startsWith('[') && query.endsWith(']')) {
26
+ try {
27
+ const parsedArray = JSON.parse(query);
28
+ if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
29
+ finalQuery = parsedArray;
30
+ finalQueryType = 'vector';
31
+ } else {
32
+ finalQueryType = 'text';
33
+ }
34
+ } catch (e) {
35
+ finalQueryType = 'text';
36
+ }
37
+ } else {
38
+ finalQueryType = 'text';
39
+ }
40
+ } else if (Array.isArray(query) && query.every(item => typeof item === 'number')) {
41
+ finalQueryType = 'vector';
42
+ } else {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: 'Error: Unable to determine query type. Please specify query_type parameter or provide a valid string (for text) or number array (for vector).',
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ }
53
+
54
+ // Handle vector query type with string input
55
+ if (finalQueryType === 'vector' && typeof query === 'string') {
56
+ try {
57
+ const parsedArray = JSON.parse(query);
58
+ if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
59
+ finalQuery = parsedArray;
60
+ } else {
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: 'Error: Vector query type requires an array of numbers',
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ } catch (e) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: 'Error: Vector query type requires an array of numbers',
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ }
81
+
82
+ // Validate query format matches query type
83
+ if (finalQueryType === 'text' && typeof finalQuery !== 'string') {
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: 'Error: Text query type requires a string query. Example: "your search text here"',
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ if (finalQueryType === 'vector' && (!Array.isArray(finalQuery) || !finalQuery.every(item => typeof item === 'number'))) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: 'Error: Vector query type requires an array of numbers. Example: [0.1, 0.2, 0.3, 0.4, 0.5] for 5-dimensional namespace',
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ const requestBody = {
105
+ namespaces,
106
+ query: finalQuery,
107
+ top_k,
108
+ kiosk_mode,
109
+ };
110
+ if (threshold !== undefined) {
111
+ requestBody.threshold = threshold;
112
+ }
113
+
114
+ const data = await makeApiRequest('POST', API_ENDPOINTS.search, requestBody);
115
+
116
+ if (!data.results || data.results.length === 0) {
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: `No results found for query: "${query}"`,
122
+ },
123
+ ],
124
+ };
125
+ }
126
+
127
+ const formattedResults = data.results.map((result, index) =>
128
+ [
129
+ `Result ${index + 1}:`,
130
+ `ID: ${result.id || 'N/A'}`,
131
+ `Text: ${result.text || result.content || 'N/A'}`,
132
+ `Score: ${result.score || 'N/A'}`,
133
+ `Label: ${result.label || 'N/A'}`,
134
+ `Metadata: ${JSON.stringify(result.metadata || {}, null, 2)}`,
135
+ "---",
136
+ ].join("\n")
137
+ );
138
+
139
+ let searchText = `Search results for "${query}" in namespaces [${namespaces.join(', ')}]:\n\n${formattedResults.join("\n")}\n\nTotal results: ${data.total || data.results.length}`;
140
+
141
+ // Add execution time and performance metrics if available
142
+ if (data.execution_time) {
143
+ searchText += `\n\nExecution time: ${data.execution_time}s`;
144
+ }
145
+
146
+ if (data.timings) {
147
+ searchText += `\n\nPerformance breakdown:\n${Object.entries(data.timings)
148
+ .filter(([key]) => key !== 'total')
149
+ .map(([key, value]) => ` ${key}: ${value}s`)
150
+ .join('\n')}`;
151
+ }
152
+
153
+ if (data.optimization_info) {
154
+ searchText += `\n\nOptimization: ${data.optimization_info.fetch_strategy}`;
155
+ }
156
+
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: searchText,
162
+ },
163
+ ],
164
+ };
165
+ } catch (error) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Error searching: ${error.message}`,
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ },
176
+ };
177
+
178
+ // Answer tool
179
+ export const answerTool = {
180
+ name: "answer",
181
+ description: "Get AI-generated answers based on data in a namespace using text queries. This tool provides intelligent, context-aware responses by searching through your stored text documents and generating comprehensive answers using advanced language models. Supports two modes: Search Mode (with namespace) and Direct AI Mode (empty namespace).",
182
+ parameters: {
183
+ namespace: z.string().describe("Namespace to answer questions from. For Search Mode: provide a text namespace containing documents to search for context. For Direct AI Mode: provide empty string \"\" to make direct AI model calls without searching your data."),
184
+ query: z.string().min(1).describe("Text query for AI answer generation. Provide a natural language question or prompt that you want the AI to answer. The AI will search through your namespace content and generate a comprehensive response based on the relevant information found."),
185
+ top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many relevant documents the AI considers when generating an answer. Default is 5. Use lower values (3-5) for focused answers, higher values (8-10) for comprehensive responses that consider more context."),
186
+ threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters documents based on relevance before generating the answer. Higher values (0.7-0.9) ensure only highly relevant content is used, lower values (0.3-0.5) include more context. Required when kiosk_mode is true."),
187
+ kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled and focused answers suitable for production environments."),
188
+ aiModel: z.string().optional().describe("AI model to use for answer generation. Different models may have different capabilities, response styles, and performance characteristics. Supported AI models include: 'anthropic.claude-3-7-sonnet-20250219-v1:0' (Claude 3.7 Sonnet), 'anthropic.claude-sonnet-4-20250514-v1:0' (Claude Sonnet 4), 'meta.llama4-maverick-17b-instruct-v1:0' (Llama 4 Maverick), 'meta.llama3-3-70b-instruct-v1:0' (Llama 3.3 70B), 'deepseek.r1-v1:0' (DeepSeek R1). If not specified, defaults to Claude 3.7 Sonnet."),
189
+ chatHistory: z.array(z.object({
190
+ role: z.string().describe("Role of the message in the conversation. Use 'user' for user messages and 'assistant' for AI responses. This helps maintain conversation context and allows the AI to reference previous exchanges."),
191
+ content: z.string()
192
+ })).optional().describe("Chat history for AI answer generation. Provide previous conversation context to help the AI maintain continuity and reference earlier parts of the conversation. This enables more coherent multi-turn conversations."),
193
+ headerPrompt: z.string().optional().describe("Header prompt for AI answer generation. Custom instructions that define the AI's role, style, and behavior. Use this to create specialized assistants (e.g., technical support, friendly helper, formal advisor) or set specific guidelines for response generation."),
194
+ footerPrompt: z.string().optional().describe("Footer prompt for AI answer generation. Additional instructions that are applied after the main response generation. Useful for formatting requirements, citation styles, or specific response patterns that should be consistently applied."),
195
+ temperature: z.number().min(0).max(2.0).optional().describe("Temperature for AI answer generation. Controls the creativity and randomness of responses. Lower values (0.1-0.3) produce more focused, deterministic answers. Higher values (0.7-1.0) produce more creative, varied responses. Default is 0.7."),
196
+ },
197
+ handler: async ({ namespace, query, top_k = 5, threshold, kiosk_mode = false, aiModel, chatHistory = [], headerPrompt, footerPrompt, temperature = 0.7 }) => {
198
+ try {
199
+ // Determine if this is Direct AI Mode (empty namespace) or Search Mode (with namespace)
200
+ const isDirectAIMode = namespace === "";
201
+
202
+ const requestBody = {
203
+ namespace,
204
+ query,
205
+ };
206
+
207
+ if (isDirectAIMode) {
208
+ // Direct AI Mode: Only allow basic AI fields
209
+ if (aiModel) {
210
+ requestBody.aiModel = aiModel;
211
+ }
212
+ if (chatHistory && chatHistory.length > 0) {
213
+ requestBody.chatHistory = chatHistory;
214
+ }
215
+ if (headerPrompt) {
216
+ requestBody.headerPrompt = headerPrompt;
217
+ }
218
+ if (footerPrompt) {
219
+ requestBody.footerPrompt = footerPrompt;
220
+ }
221
+ if (temperature !== undefined) {
222
+ requestBody.temperature = temperature;
223
+ }
224
+ } else {
225
+ // Search Mode: Allow all fields including search parameters
226
+ requestBody.top_k = top_k;
227
+ requestBody.kiosk_mode = kiosk_mode;
228
+
229
+ if (threshold !== undefined) {
230
+ requestBody.threshold = threshold;
231
+ }
232
+ if (aiModel) {
233
+ requestBody.aiModel = aiModel;
234
+ }
235
+ if (chatHistory && chatHistory.length > 0) {
236
+ requestBody.chatHistory = chatHistory;
237
+ }
238
+ if (headerPrompt) {
239
+ requestBody.headerPrompt = headerPrompt;
240
+ }
241
+ if (footerPrompt) {
242
+ requestBody.footerPrompt = footerPrompt;
243
+ }
244
+ if (temperature !== undefined) {
245
+ requestBody.temperature = temperature;
246
+ }
247
+ }
248
+
249
+ const data = await makeApiRequest('POST', API_ENDPOINTS.answer, requestBody);
250
+
251
+ const mode = isDirectAIMode ? "Direct AI Mode" : "Search Mode";
252
+ const namespaceInfo = isDirectAIMode ? "no namespace (direct AI call)" : `namespace "${namespace}"`;
253
+ const resultText = `AI Answer (${mode}) for "${query}" using ${namespaceInfo}:\n\n${data.answer || data.response || JSON.stringify(data, null, 2)}`;
254
+
255
+ return {
256
+ content: [
257
+ {
258
+ type: "text",
259
+ text: resultText,
260
+ },
261
+ ],
262
+ };
263
+ } catch (error) {
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text",
268
+ text: `Error getting AI answer: ${error.message}`,
269
+ },
270
+ ],
271
+ };
272
+ }
273
+ },
230
274
  };