npm package: @qicaixin/quick-pdf-search
Description: MCP server for fast PDF parsing and BM25 search via MinerU.
Latest version: 1.0.2

# @qicaixin/quick-pdf-search

MCP (Model Context Protocol) server for fast PDF parsing and search. It uses the MinerU API to parse PDFs, stores the parsed output locally, and provides BM25 search over `layout.json` using wink-nlp + wink-bm25-text-search.

## Features
- MCP stdio server with `parse_pdf` and `search_pdf` tools
- MinerU API integration (upload, poll, download, extract)
- Cache by file-content hash + meta validation to avoid duplicate API calls
- Search uses `layout.json` only (no fallback to `content_list`)

## Requirements
- Node.js 18+ (tested on Node 22)
- MinerU API token

## Install
```bash
npm install
```

## Use via npx (after publishing)
```bash
npx @qicaixin/quick-pdf-search
```

## Use via npm install
### Global install
```bash
npm install -g @qicaixin/quick-pdf-search
quick-pdf-search
```

### Local dependency
```bash
npm install @qicaixin/quick-pdf-search
npx @qicaixin/quick-pdf-search
```

## Install directly from GitHub
```bash
npm install github:labveritas/quick-pdf-search
```

With branch or tag:
```bash
npm install github:labveritas/quick-pdf-search#main
```

## Run the MCP server (stdio)
```bash
npm start
```

## Environment variables
- `MINERU_TOKEN` (required for parsing)
- `QPS_OUTPUT_ROOT` (optional) output root directory  
  Default: `~/.cache/quick-pdf-search-mcp`
- `MINERU_REQUEST_TIMEOUT_MS` (optional) per-request timeout for MinerU API calls  
  Default: `30000`

## Tools

### `parse_pdf`
Parse a local PDF via MinerU and cache results on disk.

Input:
- `file_path` (string, required)
- `data_id` (string, optional) override cache key
- `output_root` (string, optional) override output root dir
- `model_version` (string, optional) MinerU model version (default: `pipeline`)
- `base_url` (string, optional) MinerU API base URL
- `timeout` (number, optional) total wait timeout in seconds
- `poll_interval` (number, optional) polling interval in seconds
- `keep_zip` (boolean, optional) keep the downloaded zip
- `force` (boolean, optional) re-parse even if cached
- `token` (string, optional) MinerU token (falls back to `MINERU_TOKEN`)

Output:
- `file_path`
- `data_id` (derived from file content hash by default)
- `output_dir`
- `layout_path`
- `content_blocks` (para_blocks total)

### `search_pdf`
Search parsed PDFs with BM25.

Input:
- `query` (string, required)
- `output_dir` (string, optional) directory containing `layout.json` (or a direct `layout.json` path)
- `top_k` (number, optional, 1-50)

Output:
- `results` array with `text`, `snippet`, `page_idx`, `block_index`, etc.

### `list_cached_pdfs`
List cached parsed PDFs under the output root.

Input:
- `output_root` (string, optional) override output root dir

Output:
- `items` array with `data_id`, `file_path`, `output_dir`, `layout_path`, `content_blocks`, etc.

### `get_page_content`
Return content for a specific page index.

Input:
- `page_idx` (number, required, 0-based)
- `output_dir` (string, optional) directory containing `layout.json` (or a direct `layout.json` path)

Output:
- `combined_text` (string)
- `blocks` array with `block_index`, `type`, `text`, `bbox`

## Cache behavior
- `data_id` is derived from file content SHA256 by default.
- `output/<data_id>/meta.json` stores `hash`, `size`, `mtimeMs`.
- If `layout.json` exists and `meta.json` matches the current file hash+size, parsing is skipped.
- Use `force: true` to re-parse.

## Quick test (stdio client)
```bash
export MINERU_TOKEN=your_token

node --input-type=module - <<'JS'
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const transport = new StdioClientTransport({
  command: 'node',
  args: ['src/index.js'],
  cwd: process.cwd(),
  env: { ...process.env },
  stderr: 'inherit'
});
const client = new Client({ name: 'test-client', version: '0.0.1' });
await client.connect(transport);

const parse = await client.request({
  method: 'tools/call',
  params: { name: 'parse_pdf', arguments: { file_path: '/absolute/path/to/file.pdf' } }
}, CallToolResultSchema, { timeout: 600000 });
console.log(parse.content[0].text);

const search = await client.request({
  method: 'tools/call',
  params: { name: 'search_pdf', arguments: { query: 'testplan' } }
}, CallToolResultSchema);
console.log(search.content[0].text);

await transport.close();
JS
```

## Project layout
```
src/
  index.js      # MCP server + search
  mineru.js     # MinerU API client + caching
output/         # (optional) local parsed outputs if you override output_root
```

## Release (Git tag for npm publish via GitHub Actions)
This repo is configured to publish on tag push `v*`.

```bash
# 1) bump version
npm version patch --no-git-tag-version

# 2) commit version bump
git add package.json package-lock.json
git commit -m "bump version"

# 3) push commit
git push

# 4) create tag and push
git tag vX.Y.Z
git push origin vX.Y.Z
```

Notes:
- The GitHub Action uses `NPM_TOKEN` secret and publishes `--access public`.
- Tag version must match `package.json` version.