{"_id":"@anyshift/mcp-tools-common","_rev":"5-58f415865c418e7aad6ab62b08c673e6","name":"@anyshift/mcp-tools-common","dist-tags":{"latest":"0.3.2"},"versions":{"0.1.0":{"name":"@anyshift/mcp-tools-common","version":"0.1.0","keywords":["mcp","jq","json","file-writer","model-context-protocol"],"author":{"name":"Anyshift"},"license":"MIT","_id":"@anyshift/mcp-tools-common@0.1.0","maintainers":[{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},{"name":"sjourdan","email":"stephane.jourdan@outlook.com"},{"name":"pcholl22","email":"pierre.chollet@anyshift.io"}],"homepage":"https://github.com/anyshift-io/mcp-tools-common#readme","bugs":{"url":"https://github.com/anyshift-io/mcp-tools-common/issues"},"dist":{"shasum":"aab24bf7a370f8bef45637d9974cf2155b052d17","tarball":"https://registry.npmjs.org/@anyshift/mcp-tools-common/-/mcp-tools-common-0.1.0.tgz","fileCount":9,"integrity":"sha512-FplMiKc/Ydpet0vm3NiGSO644p6h7XrzbYkK3DDURIR0OelYua9NR3wopg4AgC9IzUad7R0CxvqxiaIA453oKQ==","signatures":[{"sig":"MEQCIG0A1S3gZ0IXjm7/N2MMBjtUQ76mWVKzI/fBSpJV2a5wAiApjRHptxnKONUgVnzsN8xgdHCwYtVM/H5CRiQzRuZxQQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":174782},"main":"dist/index.js","type":"module","_from":"file:anyshift-mcp-tools-common-0.1.0.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"eslint src --ext .ts","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\"","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},"_resolved":"/tmp/87779b8806f6f7e0f4c43460ab7205cd/anyshift-mcp-tools-common-0.1.0.tgz","_integrity":"sha512-FplMiKc/Ydpet0vm3NiGSO644p6h7XrzbYkK3DDURIR0OelYua9NR3wopg4AgC9IzUad7R0CxvqxiaIA453oKQ==","repository":{"url":"git+https://github.com/anyshift-io/mcp-tools-common.git","type":"git"},"_npmVersion":"10.9.3","description":"Reusable JQ tool and file writing utilities for MCP servers","directories":{},"_nodeVersion":"22.20.0","dependencies":{"zod":"^3.24.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.0.0","typescript":"^5.0.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.0.0","@modelcontextprotocol/sdk":"^1.7.0"},"peerDependencies":{"@modelcontextprotocol/sdk":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mcp-tools-common_0.1.0_1760519217622_0.31056968795690554","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@anyshift/mcp-tools-common","version":"0.3.1","keywords":["mcp","jq","json","file-writer","model-context-protocol"],"author":{"name":"Anyshift"},"license":"MIT","_id":"@anyshift/mcp-tools-common@0.3.1","maintainers":[{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},{"name":"sjourdan","email":"stephane.jourdan@outlook.com"},{"name":"pcholl22","email":"pierre.chollet@anyshift.io"}],"homepage":"https://github.com/anyshift-io/mcp-tools-common#readme","bugs":{"url":"https://github.com/anyshift-io/mcp-tools-common/issues"},"dist":{"shasum":"8e16dd1d1033f6fddd32178d9a0f8bf1d86ef553","tarball":"https://registry.npmjs.org/@anyshift/mcp-tools-common/-/mcp-tools-common-0.3.1.tgz","fileCount":9,"integrity":"sha512-0vo5J6WQVQF0n5Qs7Z15A17i+XUXZhZvJCp5gN+t8ZcwFpwM0CsToZwrwhc7bqeS0aGzoU3TWAEhKIt/6RD6uQ==","signatures":[{"sig":"MEQCIF0Yh7iWpeGITxbZ03MO3JvmkJRWmzvMKuvqeB/0xcn4AiAc4ftyUNYKUNszYsEIyXpnTxAcDQJFUX89Bhp5nrz+ew==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":196751},"main":"dist/index.js","type":"module","_from":"file:anyshift-mcp-tools-common-0.3.1.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"eslint src --ext .ts","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\"","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},"_resolved":"/tmp/63d7141e3e26a06da1e2838249ca0a95/anyshift-mcp-tools-common-0.3.1.tgz","_integrity":"sha512-0vo5J6WQVQF0n5Qs7Z15A17i+XUXZhZvJCp5gN+t8ZcwFpwM0CsToZwrwhc7bqeS0aGzoU3TWAEhKIt/6RD6uQ==","repository":{"url":"git+https://github.com/anyshift-io/mcp-tools-common.git","type":"git"},"_npmVersion":"10.9.3","description":"Reusable JQ tool and file writing utilities for MCP servers","directories":{},"_nodeVersion":"22.20.0","dependencies":{"zod":"^3.24.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.0.0","typescript":"^5.0.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.0.0","@modelcontextprotocol/sdk":"^1.7.0"},"peerDependencies":{"@modelcontextprotocol/sdk":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mcp-tools-common_0.3.1_1760530486671_0.15479653634237467","host":"s3://npm-registry-packages-npm-production"}},"0.3.2":{"name":"@anyshift/mcp-tools-common","version":"0.3.2","keywords":["mcp","jq","json","file-writer","model-context-protocol"],"author":{"name":"Anyshift"},"license":"MIT","_id":"@anyshift/mcp-tools-common@0.3.2","maintainers":[{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},{"name":"sjourdan","email":"stephane.jourdan@outlook.com"},{"name":"pcholl22","email":"pierre.chollet@anyshift.io"}],"homepage":"https://github.com/anyshift-io/mcp-tools-common#readme","bugs":{"url":"https://github.com/anyshift-io/mcp-tools-common/issues"},"dist":{"shasum":"5a578ac26f9f4fcc99db49602578f0994ee6cfab","tarball":"https://registry.npmjs.org/@anyshift/mcp-tools-common/-/mcp-tools-common-0.3.2.tgz","fileCount":9,"integrity":"sha512-OIjLDfqlcQjDKhpdgHYTv45O4kih8zp4Brs4CRClAaspsZY3HaqkOaEKuIJdBd969C1bJ3pYzsB5TAfsN8LqJA==","signatures":[{"sig":"MEYCIQCg9jCbfMjFriwcdMRpLyOLdYBLXCGkw9lJWhNmZPZV3QIhAMs23TgP37OwIaHGt8P/lCEDcJkK0GEMr5vbd/5gxlq0","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":196751},"main":"dist/index.js","type":"module","_from":"file:anyshift-mcp-tools-common-0.3.2.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"eslint src --ext .ts","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\"","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"safwentrabelsi","email":"safwentrabelsi95@gmail.com"},"_resolved":"/tmp/90e997530cf496650222ef8236df3790/anyshift-mcp-tools-common-0.3.2.tgz","_integrity":"sha512-OIjLDfqlcQjDKhpdgHYTv45O4kih8zp4Brs4CRClAaspsZY3HaqkOaEKuIJdBd969C1bJ3pYzsB5TAfsN8LqJA==","repository":{"url":"git+https://github.com/anyshift-io/mcp-tools-common.git","type":"git"},"_npmVersion":"10.9.3","description":"Reusable JQ tool and file writing utilities for MCP servers","directories":{},"_nodeVersion":"22.20.0","dependencies":{"zod":"^3.24.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.0.0","typescript":"^5.0.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.0.0","@modelcontextprotocol/sdk":"^1.7.0"},"peerDependencies":{"@modelcontextprotocol/sdk":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mcp-tools-common_0.3.2_1760531148065_0.22613950699769014","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2025-10-15T09:06:57.556Z","modified":"2026-07-22T19:12:36.819Z","0.1.0":"2025-10-15T09:06:57.811Z","0.3.1":"2025-10-15T12:14:46.864Z","0.3.2":"2025-10-15T12:25:48.287Z"},"bugs":{"url":"https://github.com/anyshift-io/mcp-tools-common/issues"},"author":{"name":"Anyshift"},"license":"MIT","homepage":"https://github.com/anyshift-io/mcp-tools-common#readme","keywords":["mcp","jq","json","file-writer","model-context-protocol"],"repository":{"url":"git+https://github.com/anyshift-io/mcp-tools-common.git","type":"git"},"description":"Reusable JQ tool and file writing utilities for MCP servers","maintainers":[{"email":"safwentrabelsi95@gmail.com","name":"safwentrabelsi"},{"email":"stephane.jourdan@outlook.com","name":"sjourdan"},{"email":"pierre.chollet@anyshift.io","name":"pcholl22"}],"readme":"# @anyshift/mcp-tools-common\n\nReusable utilities for building MCP (Model Context Protocol) servers. Provides production-ready tools for JSON processing and intelligent response handling.\n\n## What's Included\n\n### 🔧 JQ Query Tool\nExecute [jq](https://jqlang.github.io/jq/) queries on JSON files with AI-optimized error messages and schema hints.\n\n**Features:**\n- Sandboxed jq execution with timeout protection\n- Path validation for security (no arbitrary file access)\n- Query sanitization (blocks environment variable access)\n- Schema-aware error messages that help LLMs write better queries\n- Comprehensive retry strategies in tool descriptions\n\n### 📄 Smart File Writer\nAutomatically write large tool responses to files instead of returning them inline.\n\n**Features:**\n- Threshold-based file writing (configurable character limit)\n- JSON schema analysis and quick reference generation\n- Compact, timestamped filenames with tool abbreviations\n- Nullable field detection for better JQ query guidance\n- Returns file references with schema hints instead of massive text\n\n### 🔍 JSON Schema Analyzer\nDeep schema analysis for JSON data structures.\n\n**Features:**\n- Detects numeric string keys (common in Cypher results: `{\"0\": {...}, \"1\": {...}}`)\n- Identifies nullable vs always-null fields\n- Handles mixed-type arrays, nested objects, and complex structures\n- Generates LLM-friendly access patterns and hints\n\n### 🛡️ Path Validation\nSecure file path validation with allowlist support.\n\n**Features:**\n- Requires absolute paths (prevents relative path ambiguity)\n- Validates files exist and are within allowed directories\n- Resolves symlinks for security\n- Clear error messages with examples\n\n### ✂️ Response Truncation\nAutomatic token-based response size limiting to prevent LLM context overflow.\n\n**Features:**\n- Token estimation using configurable chars/token ratio\n- Configurable token limits (e.g., 10k for Datadog, 15k for Anyshift)\n- Optional JSON logging to stderr for monitoring\n- Customizable truncation notice messages\n- Preserves as much content as possible before truncating\n\n## Installation\n\n```bash\nnpm install @anyshift/mcp-tools-common\n# or\npnpm add @anyshift/mcp-tools-common\n```\n\n## Integration Guide\n\n### Option 1: Using High-Level Factory Functions (Recommended)\n\nThe easiest way to integrate - use `createJqTool()` and `createFileWriter()`:\n\n```typescript\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { createJqTool, createFileWriter } from '@anyshift/mcp-tools-common';\n\nconst server = new McpServer({\n  name: 'my-mcp-server',\n  version: '1.0.0',\n});\n\n// 1. Configure File Writer\nconst fileWriter = createFileWriter({\n  enabled: process.env.WRITE_TO_FILE === 'true',\n  outputPath: process.env.OUTPUT_PATH || './output',\n  minCharsForWrite: 1000,  // Write to file if response > 1000 chars\n  toolAbbreviations: {\n    'my_data_tool': 'data',\n    'my_search_tool': 'srch',\n  }\n});\n\n// 2. Configure JQ Tool\nconst jqTool = createJqTool({\n  allowedPaths: [\n    process.cwd(),  // Allow files in execution directory\n    process.env.OUTPUT_PATH || './output',  // Allow files in output directory\n  ],\n  timeoutMs: 30000,  // 30 second timeout\n});\n\n// 3. Configure Response Truncation\nimport { truncateResponseIfNeeded, type TruncationConfig } from '@anyshift/mcp-tools-common';\n\nconst truncationConfig: TruncationConfig = {\n  maxTokens: 15000,  // Max tokens before truncation\n  enableLogging: false,  // Optional: log truncation events to stderr\n  charsPerToken: 4,  // Token estimation ratio (default: 4)\n};\n\n// 4. Register JQ Tool with MCP Server\nserver.tool(\n  jqTool.toolDefinition.name,\n  jqTool.toolDefinition.description,\n  jqTool.toolDefinition.inputSchema,\n  async (args) => {\n    return await jqTool.handler({ params: { arguments: args } });\n  }\n);\n\n// 5. Wrap Your Other Tools with File Writer and Truncation\nserver.tool(\n  'my_data_tool',\n  'Fetch large datasets',\n  { query: { type: 'string' } },\n  async ({ query }) => {\n    const result = await fetchData(query);\n\n    // handleResponse will:\n    // - Return result inline if small\n    // - Write to file and return file reference if large\n    let response = await fileWriter.handleResponse(\n      'my_data_tool',\n      { query },\n      result\n    );\n\n    // Apply truncation to prevent context overflow\n    if (response.content?.[0]?.text) {\n      response.content[0].text = truncateResponseIfNeeded(\n        truncationConfig,\n        response.content[0].text\n      );\n    }\n\n    return response;\n  }\n);\n```\n\n### Option 2: Using Individual Functions (Advanced)\n\nFor more control, import and use functions directly:\n\n```typescript\nimport {\n  executeJqQuery,\n  handleToolResponse,\n  analyzeJsonSchema,\n  generateCompactFilename,\n  validatePathWithinAllowedDirs,\n  type JqConfig,\n  type FileWriterConfig\n} from '@anyshift/mcp-tools-common';\n\n// Define your configs\nconst jqConfig: JqConfig = {\n  allowedPaths: ['/path/to/data'],\n  timeoutMs: 30000,\n};\n\nconst fileWriterConfig: FileWriterConfig = {\n  enabled: true,\n  outputPath: '/tmp/output',\n  minCharsForWrite: 500,\n  toolAbbreviations: { 'my_tool': 'mt' }\n};\n\n// Use functions directly\nserver.tool('execute_jq_query', 'Run jq queries', schema, async ({ jq_query, file_path }) => {\n  return await executeJqQuery(jqConfig, jq_query, file_path);\n});\n\nserver.tool('my_tool', 'Example tool', schema, async (args) => {\n  const response = { content: [{ type: 'text', text: 'Large data...' }] };\n  return await handleToolResponse(fileWriterConfig, 'my_tool', args, response);\n});\n```\n\n### Option 3: Zero Dependencies on Environment Variables\n\nPass configuration explicitly for maximum flexibility:\n\n```typescript\nimport { executeJqQuery } from '@anyshift/mcp-tools-common';\n\n// No environment variables - pure configuration\nconst result = await executeJqQuery(\n  {\n    allowedPaths: ['/specific/path'],\n    timeoutMs: 10000,\n  },\n  '.data[] | select(.active == true)',\n  '/specific/path/data.json'\n);\n```\n\n## API Reference\n\n### JQ Tool\n\n#### `executeJqQuery(config, jqQuery, filePath)`\n\nExecute a jq query on a JSON file.\n\n```typescript\nimport { executeJqQuery, type JqConfig } from '@anyshift/mcp-tools-common';\n\nconst config: JqConfig = {\n  allowedPaths: ['/data'],\n  timeoutMs: 30000,\n};\n\nconst result = await executeJqQuery(\n  config,\n  '.users[] | select(.age > 18)',\n  '/data/users.json'\n);\n// Returns: { content: [{ type: 'text', text: '...' }] }\n```\n\n**Parameters:**\n- `config: JqConfig` - Configuration object\n  - `allowedPaths: string[]` - Absolute paths where files can be accessed\n  - `timeoutMs: number` - Maximum execution time in milliseconds\n- `jqQuery: string` - The jq query to execute (will be sanitized)\n- `filePath: string` - Absolute path to JSON file (must be in allowedPaths)\n\n**Returns:** `Promise<{ content: Array<{ type: 'text'; text: string }> }>`\n\n#### `createJqTool(config)`\n\nCreate a JQ tool with handler and definition.\n\n```typescript\nconst jqTool = createJqTool({ allowedPaths: ['/data'], timeoutMs: 30000 });\n\n// Use with MCP SDK\nserver.tool(\n  jqTool.toolDefinition.name,\n  jqTool.toolDefinition.description,\n  jqTool.toolDefinition.inputSchema,\n  async (args) => jqTool.handler({ params: { arguments: args } })\n);\n```\n\n### File Writer\n\n#### `handleToolResponse(config, toolName, args, responseData)`\n\nIntelligently handle tool responses - write to file if large, return inline if small.\n\n```typescript\nimport { handleToolResponse, type FileWriterConfig } from '@anyshift/mcp-tools-common';\n\nconst config: FileWriterConfig = {\n  enabled: true,\n  outputPath: '/tmp/output',\n  minCharsForWrite: 1000,\n  toolAbbreviations: { 'search_data': 'srch' }\n};\n\nconst response = {\n  content: [{ type: 'text', text: 'Very large dataset...' }],\n  _rawText: 'Very large dataset...'  // Optional: for better file writing\n};\n\nconst result = await handleToolResponse(\n  config,\n  'search_data',\n  { query: 'users' },\n  response\n);\n// If large: { content: [{ type: 'text', text: '📄 File: /tmp/output/srch-20250115-1234-a1b2.json\\n...' }] }\n// If small: returns response as-is\n```\n\n**Parameters:**\n- `config: FileWriterConfig` - Configuration object\n  - `enabled: boolean` - Enable file writing\n  - `outputPath: string` - Directory for output files\n  - `minCharsForWrite: number` - Minimum characters to trigger file write\n  - `toolAbbreviations: Record<string, string>` - Tool name abbreviations for filenames\n- `toolName: string` - Name of the tool (for filename generation)\n- `args: Record<string, unknown>` - Tool arguments (for filename hash)\n- `responseData: unknown` - The response to potentially write to file\n\n**Returns:** `Promise<FileWriterResult | unknown>` - Either file reference or original response\n\n#### `createFileWriter(config)`\n\nCreate a file writer instance.\n\n```typescript\nconst fileWriter = createFileWriter({\n  enabled: true,\n  outputPath: './output',\n  minCharsForWrite: 1000,\n  toolAbbreviations: { 'my_tool': 'mt' }\n});\n\nconst result = await fileWriter.handleResponse('my_tool', { arg: 'value' }, response);\n```\n\n### Schema Analysis\n\n#### `analyzeJsonSchema(data)`\n\nAnalyze JSON structure and generate schema with LLM-friendly hints.\n\n```typescript\nimport { analyzeJsonSchema } from '@anyshift/mcp-tools-common';\n\nconst data = {\n  \"0\": { Values: [1, \"Alice\", null], Keys: [\"id\", \"name\", \"email\"] },\n  \"1\": { Values: [2, \"Bob\", \"bob@example.com\"], Keys: [\"id\", \"name\", \"email\"] }\n};\n\nconst schema = analyzeJsonSchema(data);\nconsole.log(schema);\n// {\n//   type: 'object',\n//   _keysAreNumeric: true,\n//   _accessPattern: 'Use .[\"0\"] not .[0]',\n//   properties: { ... }\n// }\n```\n\n**Returns:** `JsonSchema` with special fields:\n- `_keysAreNumeric: boolean` - Object has numeric string keys (e.g., Cypher results)\n- `_accessPattern: string` - Hint for accessing data\n- `_hasNulls: boolean` - Contains null values (suggests filtering)\n- `_hint: string` - Suggestion for handling data\n\n#### `extractNullableFields(schema)`\n\nExtract nullable field information from schema.\n\n```typescript\nimport { analyzeJsonSchema, extractNullableFields } from '@anyshift/mcp-tools-common';\n\nconst schema = analyzeJsonSchema(data);\nconst nullFields = extractNullableFields(schema);\n// {\n//   alwaysNull: ['email'],        // Fields that are always null\n//   nullable: ['middleName']       // Fields that are sometimes null\n// }\n```\n\n### Path Validation\n\n#### `validatePathWithinAllowedDirs(filePath, allowedPaths)`\n\nValidate that a file path is within allowed directories.\n\n```typescript\nimport { validatePathWithinAllowedDirs } from '@anyshift/mcp-tools-common';\n\ntry {\n  const realPath = validatePathWithinAllowedDirs(\n    '/data/users.json',\n    ['/data', '/tmp']\n  );\n  console.log('Access granted:', realPath);\n} catch (error) {\n  console.error('Access denied:', error.message);\n}\n```\n\n**Throws:** Error if:\n- Path is not absolute\n- File doesn't exist\n- File is outside allowed directories\n\n### Response Truncation\n\n#### `truncateResponseIfNeeded(config, content)`\n\nTruncate response content if it exceeds the configured token limit.\n\n```typescript\nimport { truncateResponseIfNeeded, type TruncationConfig } from '@anyshift/mcp-tools-common';\n\nconst config: TruncationConfig = {\n  maxTokens: 15000,\n  enableLogging: false,  // Set to true for JSON logs to stderr\n  charsPerToken: 4,      // Default: 4 chars per token\n};\n\nconst content = 'Very large response text...';\nconst truncated = truncateResponseIfNeeded(config, content);\n// Returns original if under limit, or truncated with notice if over\n```\n\n**Parameters:**\n- `config: TruncationConfig` - Configuration object\n  - `maxTokens: number` - Maximum allowed tokens (e.g., 10000, 15000)\n  - `enableLogging?: boolean` - Log truncation events to stderr as JSON (default: false)\n  - `messagePrefix?: string` - Custom prefix for truncation notice (default: \"RESPONSE TRUNCATED\")\n  - `charsPerToken?: number` - Characters per token ratio (default: 4)\n- `content: string` - The content to potentially truncate\n\n**Returns:** `string` - Original content if under limit, or truncated content with notice\n\n#### `estimateTokens(text, charsPerToken?)`\n\nEstimate token count using chars/token ratio.\n\n```typescript\nimport { estimateTokens } from '@anyshift/mcp-tools-common';\n\nconst tokens = estimateTokens('Hello world', 4);\n// Returns: 3 (11 chars / 4 = 2.75, rounded up to 3)\n```\n\n**Parameters:**\n- `text: string` - Text to estimate tokens for\n- `charsPerToken?: number` - Characters per token ratio (default: 4)\n\n**Returns:** `number` - Estimated token count\n\n#### `wouldBeTruncated(content, maxTokens, charsPerToken?)`\n\nCheck if content would be truncated without actually truncating.\n\n```typescript\nimport { wouldBeTruncated } from '@anyshift/mcp-tools-common';\n\nif (wouldBeTruncated(content, 15000)) {\n  console.log('Content exceeds 15k tokens, will be truncated');\n}\n```\n\n**Parameters:**\n- `content: string` - Content to check\n- `maxTokens: number` - Maximum token limit\n- `charsPerToken?: number` - Characters per token ratio (default: 4)\n\n**Returns:** `boolean` - True if content exceeds token limit\n\n### Utilities\n\n#### `generateCompactFilename(toolName, args, abbreviations?)`\n\nGenerate compact, deterministic filenames for tool output.\n\n```typescript\nimport { generateCompactFilename } from '@anyshift/mcp-tools-common';\n\nconst filename = generateCompactFilename(\n  'search_users',\n  { query: 'active users', limit: 10 },\n  { 'search_users': 'srch' }\n);\n// Returns: 'srch-20250115-1430-a3f9.json'\n// Format: {abbrev}-{YYYYMMDD}-{HHMM}-{hash}.json\n```\n\n## Configuration Best Practices\n\n### Environment Variables Pattern\n\n```typescript\n// config/toolsCommon.ts - Centralize config mapping\nimport { FileWriterConfig, JqConfig, TruncationConfig } from '@anyshift/mcp-tools-common';\n\nexport const fileWriterConfig: FileWriterConfig = {\n  enabled: process.env.WRITE_TO_FILE === 'true',\n  outputPath: process.env.OUTPUT_PATH || './output',\n  minCharsForWrite: Number(process.env.MIN_CHARS_FOR_FILE_WRITE) || 1000,\n  toolAbbreviations: {\n    'my_tool': 'mt',\n    'search': 'srch',\n  }\n};\n\nexport const jqConfig: JqConfig = {\n  allowedPaths: [\n    process.cwd(),\n    process.env.OUTPUT_PATH || './output',\n  ].filter(Boolean),\n  timeoutMs: 30000,\n};\n\nexport const truncationConfig: TruncationConfig = {\n  maxTokens: 15000,  // Adjust based on your LLM's context window\n  enableLogging: process.env.TRUNCATION_LOGGING === 'true',\n  charsPerToken: 4,\n};\n\n// Use in tools:\nimport { fileWriterConfig, jqConfig, truncationConfig } from './config/toolsCommon';\n```\n\n### Security Considerations\n\n1. **Always use absolute paths** for `allowedPaths` - relative paths can be ambiguous\n2. **Validate user input** - especially for file paths and jq queries\n3. **Set reasonable timeouts** - default 30s prevents hanging queries\n4. **Limit file access** - only allow necessary directories in `allowedPaths`\n5. **JQ query sanitization is automatic** - blocks `$ENV` and `env` access\n\n### Performance Tips\n\n1. **Set appropriate `minCharsForWrite`** - balance between inline convenience and context limits\n2. **Use tool abbreviations** - keeps filenames short and readable\n3. **Consider pagination** - disable pagination when file writing is enabled\n4. **Cache resolved paths** - validate allowedPaths at startup, not per-request\n5. **Configure truncation limits** - set `maxTokens` based on your LLM's context window (e.g., 10k for Datadog, 15k for Anyshift)\n6. **Enable truncation logging selectively** - use `enableLogging: true` in development/debugging, disable in production\n\n## TypeScript Types\n\nAll major types are exported:\n\n```typescript\nimport type {\n  JqConfig,\n  FileWriterConfig,\n  FileWriterResult,\n  JsonSchema,\n  NullableFields,\n  TruncationConfig,\n} from '@anyshift/mcp-tools-common';\n```\n\n## Examples\n\n### Complete MCP Server with Both Features\n\n```typescript\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { createJqTool, createFileWriter } from '@anyshift/mcp-tools-common';\n\nconst server = new McpServer({ name: 'example-server', version: '1.0.0' });\n\n// Setup\nconst outputPath = process.env.OUTPUT_PATH || './output';\nconst fileWriter = createFileWriter({\n  enabled: process.env.WRITE_TO_FILE === 'true',\n  outputPath,\n  minCharsForWrite: 1000,\n  toolAbbreviations: { 'fetch_data': 'data', 'search': 'srch' }\n});\n\nconst jqTool = createJqTool({\n  allowedPaths: [process.cwd(), outputPath],\n  timeoutMs: 30000,\n});\n\n// Register JQ tool\nserver.tool(\n  jqTool.toolDefinition.name,\n  jqTool.toolDefinition.description,\n  jqTool.toolDefinition.inputSchema,\n  async (args) => jqTool.handler({ params: { arguments: args } })\n);\n\n// Register custom tool with file writing\nserver.tool('fetch_data', 'Fetch large datasets', {\n  query: { type: 'string' }\n}, async ({ query }) => {\n  const data = await fetchLargeDataset(query);\n  const response = {\n    content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],\n    _rawText: JSON.stringify(data, null, 2)\n  };\n\n  return await fileWriter.handleResponse('fetch_data', { query }, response);\n});\n\nserver.connect();\n```\n\n## Migrating from Inline Implementations\n\nIf you have existing JQ or file writing code in your MCP server:\n\n1. **Install the package:** `npm install @anyshift/mcp-tools-common`\n2. **Create config adapter:** Map your env vars to `JqConfig` and `FileWriterConfig`\n3. **Replace tool implementations:** Use `executeJqQuery()` or `createJqTool()`\n4. **Wrap tool responses:** Replace inline file writing with `handleToolResponse()`\n5. **Remove duplicate code:** Delete old implementations and helpers\n6. **Test thoroughly:** Verify file writing thresholds and JQ queries work\n\nSee the [Anyshift MCP Server](https://github.com/anyshift/anyshift-mcp-server) for a real-world migration example.\n\n## Requirements\n\n- Node.js >= 18.0.0\n- `jq` command-line tool installed on system (for JQ functionality)\n\n### Installing jq\n\n```bash\n# macOS\nbrew install jq\n\n# Ubuntu/Debian\napt-get install jq\n\n# Windows (via Chocolatey)\nchoco install jq\n\n# Or download from https://jqlang.github.io/jq/download/\n```\n\n## License\n\nMIT\n\n## Contributing\n\nIssues and pull requests welcome! This library is designed to be MCP-server agnostic and should work with any MCP implementation.\n\n## Related\n\n- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)\n- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)\n- [jq Manual](https://jqlang.github.io/jq/manual/)","readmeFilename":"README.md"}