@tlelabs/md-mcp 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/convert.js +183 -0
- package/dist/errors.js +23 -0
- package/dist/index.js +52 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lê Phước Lộc (TLE Labs)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tlelabs/md-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for [md. by TLE Labs](https://md.tlelabs.com): lets an AI agent (Claude Code, Cursor, Codex, Claude Desktop…) convert local PDF, DOCX, PPTX, XLSX and HTML files to Markdown.
|
|
4
|
+
|
|
5
|
+
The server runs on your machine over stdio, reads the file from disk and sends it to the md. API with your API key. Nothing is stored by the API.
|
|
6
|
+
|
|
7
|
+
## Set up
|
|
8
|
+
|
|
9
|
+
1. Sign in at [md.tlelabs.com](https://md.tlelabs.com/login) and create an API key under [API keys](https://md.tlelabs.com/keys). New accounts get free credits; buy more under [Billing](https://md.tlelabs.com/billing).
|
|
10
|
+
2. Add the server to your client with the key in `MD_API_KEY`. Node.js 20 or newer is required.
|
|
11
|
+
|
|
12
|
+
**Claude Code**
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
claude mcp add md -e MD_API_KEY=mdw_live_... -- npx -y @tlelabs/md-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Cursor, Claude Desktop and other clients using `mcpServers` JSON**
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"md": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["-y", "@tlelabs/md-mcp"],
|
|
26
|
+
"env": { "MD_API_KEY": "mdw_live_..." }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Codex** (`~/.codex/config.toml`)
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[mcp_servers.md]
|
|
36
|
+
command = "npx"
|
|
37
|
+
args = ["-y", "@tlelabs/md-mcp"]
|
|
38
|
+
env = { MD_API_KEY = "mdw_live_..." }
|
|
39
|
+
tool_timeout_sec = 600
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Long PDFs that need OCR can take a few minutes. If your client stops waiting before the conversion ends, raise its tool timeout (Codex `tool_timeout_sec`, Claude Code `MCP_TOOL_TIMEOUT` in milliseconds).
|
|
43
|
+
|
|
44
|
+
## Tool: `convert_file`
|
|
45
|
+
|
|
46
|
+
| Argument | Type | Description |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `path` | string | File to convert. Relative paths resolve against the server's working directory; absolute paths are safest |
|
|
49
|
+
| `output_path` | string, optional | Write the Markdown to this file instead of returning it (use it for long documents). Parent directories are created |
|
|
50
|
+
| `overwrite` | boolean, optional | Replace `output_path` if it exists (default `false`: an existing file is never replaced) |
|
|
51
|
+
| `ocr` | `auto` \| `force` \| `off`, optional | OCR mode (default `auto`; `force` only changes PDFs) |
|
|
52
|
+
| `ocr_model` | string, optional | `auto` (default) or a model id such as `openparser/mistral-ocr-4` |
|
|
53
|
+
| `preserve_images` | boolean, optional | Keep image placeholders with their OCR text (default `true`) |
|
|
54
|
+
|
|
55
|
+
The result holds the Markdown (or the path it was saved to) and a summary: format, pages, OCR pages, credits charged and remaining, and any warnings.
|
|
56
|
+
|
|
57
|
+
## Billing
|
|
58
|
+
|
|
59
|
+
Every call is a new conversion and is charged: 1 credit per page, more for pages read with OCR (see [pricing](https://md.tlelabs.com/docs/formats-and-pricing/)). Calling again with the same file is charged again. The server never retries on its own.
|
|
60
|
+
|
|
61
|
+
Checks that can fail locally (missing file, file over 50 MB, `output_path` already exists, parent directory cannot be created) run before upload and cost nothing.
|
|
62
|
+
|
|
63
|
+
If the connection drops, or the API answers with something unexpected, the result says the conversion may have been charged: check [Usage](https://md.tlelabs.com/usage) before converting again. Cancelling a call stops waiting for it and nothing is written, but a conversion the API already finished is still charged.
|
|
64
|
+
|
|
65
|
+
If the Markdown cannot be saved to `output_path` after a paid conversion (for example the file appeared meanwhile, or the disk is full), it is returned in the result instead and any existing file is left unchanged.
|
|
66
|
+
|
|
67
|
+
## Environment
|
|
68
|
+
|
|
69
|
+
| Variable | Required | Default |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `MD_API_KEY` | yes | — |
|
|
72
|
+
| `MD_API_BASE` | no | `https://api.md.tlelabs.com` |
|
|
73
|
+
|
|
74
|
+
The key is sent only to `MD_API_BASE` and never logged.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/dist/convert.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { openAsBlob } from "node:fs";
|
|
3
|
+
import { link, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { apiErrorText, AMBIGUOUS_CHARGE } from "./errors.js";
|
|
6
|
+
/** Same limit as the Worker (`MAX_FILE_SIZE` in src/routes/convert.ts): larger files are refused before upload. */
|
|
7
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
8
|
+
export const KEYS_URL = "https://md.tlelabs.com/keys";
|
|
9
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
11
|
+
const defaultTempName = () => `.md-mcp-${randomBytes(4).toString("hex")}.tmp`;
|
|
12
|
+
export async function convertFile(input, deps) {
|
|
13
|
+
if (!deps.apiKey) {
|
|
14
|
+
return fail(`MD_API_KEY is not set. Create an API key at ${KEYS_URL} and add it to this MCP server's env as MD_API_KEY.`);
|
|
15
|
+
}
|
|
16
|
+
const source = resolve(deps.cwd, input.path);
|
|
17
|
+
let size;
|
|
18
|
+
try {
|
|
19
|
+
const info = await stat(source);
|
|
20
|
+
if (!info.isFile())
|
|
21
|
+
return fail(`${source} is not a regular file.`);
|
|
22
|
+
size = info.size;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return fail(`Cannot read ${source}: ${e.code ?? String(e)}.`);
|
|
26
|
+
}
|
|
27
|
+
if (size > MAX_FILE_SIZE) {
|
|
28
|
+
return fail(`file_too_large: ${source} is ${size} bytes; the limit is ${MAX_FILE_SIZE} bytes (50 MB). Nothing was uploaded.`);
|
|
29
|
+
}
|
|
30
|
+
// Everything that can fail at the destination is checked before upload, so it costs no credits.
|
|
31
|
+
let destination;
|
|
32
|
+
if (input.output_path !== undefined) {
|
|
33
|
+
destination = resolve(deps.cwd, input.output_path);
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return fail(`Cannot create the directory of ${destination}: ${e.code ?? String(e)}. Nothing was uploaded.`);
|
|
39
|
+
}
|
|
40
|
+
let existing;
|
|
41
|
+
try {
|
|
42
|
+
existing = await lstat(destination);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
// Only "does not exist" means the destination is free; anything else (EACCES, ENAMETOOLONG…) fails before upload
|
|
46
|
+
const code = e.code;
|
|
47
|
+
if (code !== "ENOENT")
|
|
48
|
+
return fail(`Cannot use ${destination}: ${code ?? String(e)}. Nothing was uploaded.`);
|
|
49
|
+
}
|
|
50
|
+
if (existing?.isDirectory())
|
|
51
|
+
return fail(`${destination} is a directory. Nothing was uploaded.`);
|
|
52
|
+
if (existing && !input.overwrite) {
|
|
53
|
+
return fail(`${destination} already exists; pass overwrite: true to replace it. Nothing was uploaded.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const form = new FormData();
|
|
57
|
+
form.append("file", await openAsBlob(source), basename(source));
|
|
58
|
+
if (input.ocr !== undefined)
|
|
59
|
+
form.append("ocr", input.ocr);
|
|
60
|
+
if (input.ocr_model !== undefined)
|
|
61
|
+
form.append("ocr_model", input.ocr_model);
|
|
62
|
+
if (input.preserve_images !== undefined)
|
|
63
|
+
form.append("preserve_images", String(input.preserve_images));
|
|
64
|
+
// No automatic retry: the API does not deduplicate, so a second request is a second charge.
|
|
65
|
+
let status;
|
|
66
|
+
let retryAfter;
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
const response = await deps.fetch(`${deps.apiBase}/v1/convert`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { Authorization: `Bearer ${deps.apiKey}`, Accept: "application/json", "User-Agent": deps.userAgent },
|
|
72
|
+
body: form,
|
|
73
|
+
signal: deps.signal,
|
|
74
|
+
});
|
|
75
|
+
status = response.status;
|
|
76
|
+
retryAfter = response.headers.get("retry-after");
|
|
77
|
+
body = await response.text();
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (deps.signal?.aborted)
|
|
81
|
+
return fail(`Cancelled. ${AMBIGUOUS_CHARGE}`);
|
|
82
|
+
return fail(`Network error talking to ${deps.apiBase}: ${e.message}. ${AMBIGUOUS_CHARGE}`);
|
|
83
|
+
}
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = JSON.parse(body);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return fail(`HTTP ${status} with a non-JSON response from ${deps.apiBase}. ${AMBIGUOUS_CHARGE}`);
|
|
90
|
+
}
|
|
91
|
+
if (status < 200 || status >= 300) {
|
|
92
|
+
const error = isObject(json) ? json.error : undefined;
|
|
93
|
+
if (!isObject(error) || typeof error.code !== "string" || typeof error.message !== "string")
|
|
94
|
+
return fail(`HTTP ${status} with an unexpected response. ${AMBIGUOUS_CHARGE}`);
|
|
95
|
+
return fail(apiErrorText(status, error, retryAfter));
|
|
96
|
+
}
|
|
97
|
+
if (!isObject(json) || typeof json.markdown !== "string" || !isObject(json.usage)) {
|
|
98
|
+
return fail(`HTTP ${status} with an unexpected response. ${AMBIGUOUS_CHARGE}`);
|
|
99
|
+
}
|
|
100
|
+
const result = {
|
|
101
|
+
...json,
|
|
102
|
+
warnings: Array.isArray(json.warnings) ? json.warnings.filter((w) => typeof w === "string") : [],
|
|
103
|
+
};
|
|
104
|
+
const summary = summarize(source, result);
|
|
105
|
+
const structured = {
|
|
106
|
+
format: result.format,
|
|
107
|
+
usage: result.usage,
|
|
108
|
+
metadata: result.metadata,
|
|
109
|
+
warnings: result.warnings,
|
|
110
|
+
};
|
|
111
|
+
if (destination === undefined) {
|
|
112
|
+
return { content: [{ type: "text", text: result.markdown }, { type: "text", text: summary }], structuredContent: structured };
|
|
113
|
+
}
|
|
114
|
+
if (deps.signal?.aborted)
|
|
115
|
+
return fail(`Cancelled after conversion; ${destination} was not written. ${result.usage.credits_charged} credits were charged.`);
|
|
116
|
+
const saveError = await publish(result.markdown, destination, input.overwrite === true, deps);
|
|
117
|
+
if (saveError === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: `${summary}\nSaved to ${destination}` }],
|
|
120
|
+
structuredContent: { ...structured, output_path: destination },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (saveError === "cancelled")
|
|
124
|
+
return fail(`Cancelled after conversion; ${destination} was not written. ${result.usage.credits_charged} credits were charged.`);
|
|
125
|
+
// The conversion is paid for: hand the Markdown back rather than lose it.
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{ type: "text", text: result.markdown },
|
|
129
|
+
{ type: "text", text: `${summary}\nCould not save to ${destination} (${saveError}); the Markdown is returned above instead. Any existing file there is unchanged.` },
|
|
130
|
+
],
|
|
131
|
+
structuredContent: { ...structured, save_error: saveError },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Writes to a scratch file in the destination's directory, then publishes it atomically:
|
|
136
|
+
* rename when overwriting, hard link otherwise (an existing destination fails with EEXIST and is kept).
|
|
137
|
+
* Returns undefined on success, "cancelled", or the error code; never leaves a partial destination.
|
|
138
|
+
*/
|
|
139
|
+
async function publish(markdown, destination, overwrite, deps) {
|
|
140
|
+
const scratch = join(dirname(destination), (deps.tempName ?? defaultTempName)());
|
|
141
|
+
let handle;
|
|
142
|
+
try {
|
|
143
|
+
handle = await open(scratch, "wx");
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return e.code ?? String(e);
|
|
147
|
+
}
|
|
148
|
+
let owned = true;
|
|
149
|
+
try {
|
|
150
|
+
try {
|
|
151
|
+
await handle.writeFile(markdown, "utf8");
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await handle.close();
|
|
155
|
+
}
|
|
156
|
+
if (deps.signal?.aborted)
|
|
157
|
+
return "cancelled";
|
|
158
|
+
if (overwrite) {
|
|
159
|
+
await rename(scratch, destination);
|
|
160
|
+
owned = false;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await (deps.link ?? link)(scratch, destination);
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return e.code ?? String(e);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (owned)
|
|
172
|
+
await unlink(scratch).catch(() => { });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function summarize(source, result) {
|
|
176
|
+
const { pages, ocr_pages, credits_charged, credits_remaining } = result.usage;
|
|
177
|
+
const lines = [
|
|
178
|
+
`Converted ${basename(source)} (${result.format}): ${pages} pages, ${ocr_pages} OCR pages; charged ${credits_charged} credits, ${credits_remaining} remaining.`,
|
|
179
|
+
];
|
|
180
|
+
for (const warning of result.warnings)
|
|
181
|
+
lines.push(`Warning: ${warning}`);
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const USAGE_URL = "https://md.tlelabs.com/usage";
|
|
2
|
+
export const BILLING_URL = "https://md.tlelabs.com/billing";
|
|
3
|
+
const KEYS_URL = "https://md.tlelabs.com/keys";
|
|
4
|
+
export const AMBIGUOUS_CHARGE = `The conversion may have been charged; check ${USAGE_URL} before converting again (a new request is charged again).`;
|
|
5
|
+
/** Text for an API error envelope: the server's code and message, plus what the agent can do about it. */
|
|
6
|
+
export function apiErrorText(status, error, retryAfter) {
|
|
7
|
+
// The Worker puts the request id in details (src/errors.ts jsonError); a top-level one is accepted too
|
|
8
|
+
const requestId = error.details?.request_id ?? error.request_id;
|
|
9
|
+
const head = `${error.code}: ${error.message}${typeof requestId === "string" ? ` (request_id ${requestId})` : ""}`;
|
|
10
|
+
switch (error.code) {
|
|
11
|
+
case "invalid_api_key":
|
|
12
|
+
return `${head}\nCheck MD_API_KEY in this MCP server's env; create or rotate a key at ${KEYS_URL}.`;
|
|
13
|
+
case "insufficient_credits":
|
|
14
|
+
return `${head}\nTop up credits at ${BILLING_URL}. Nothing was charged.`;
|
|
15
|
+
case "rate_limited":
|
|
16
|
+
case "too_many_concurrent":
|
|
17
|
+
return `${head}\n${retryAfter ? `Retry after ${retryAfter} seconds.` : "Retry later."} Nothing was charged.`;
|
|
18
|
+
}
|
|
19
|
+
// A 503 carrying details.request_id means the charge could not be confirmed (docs/api.md, Errors).
|
|
20
|
+
if (status === 503 && error.details?.request_id !== undefined)
|
|
21
|
+
return `${head}\n${AMBIGUOUS_CHARGE}`;
|
|
22
|
+
return head;
|
|
23
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { convertFile } from "./convert.js";
|
|
7
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
8
|
+
const apiBase = (process.env.MD_API_BASE || "https://api.md.tlelabs.com").replace(/\/+$/, "");
|
|
9
|
+
const server = new McpServer({ name: "md", version });
|
|
10
|
+
/** Tool calls still running, awaited on shutdown. */
|
|
11
|
+
const active = new Set();
|
|
12
|
+
server.registerTool("convert_file", {
|
|
13
|
+
title: "Convert a file to Markdown",
|
|
14
|
+
description: "Convert a local PDF, DOCX, PPTX, XLSX or HTML file (up to 50 MB) to Markdown with md. by TLE Labs. " +
|
|
15
|
+
"Each call is billed in credits (1 per page; pages that need OCR cost more) and is charged again if repeated. " +
|
|
16
|
+
"Prefer absolute paths. For long documents pass output_path so the Markdown is written to a file instead of returned.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
path: z.string().min(1).describe("File to convert; relative paths resolve against the server's working directory"),
|
|
19
|
+
output_path: z.string().min(1).optional().describe("Write the Markdown to this file instead of returning it"),
|
|
20
|
+
overwrite: z.boolean().optional().describe("Replace output_path if it already exists (default false)"),
|
|
21
|
+
ocr: z.enum(["auto", "force", "off"]).optional().describe("OCR mode: auto (default), force (PDF only: OCR every page), off"),
|
|
22
|
+
ocr_model: z.string().min(1).optional().describe("OCR model: auto (default) or a provider/model id"),
|
|
23
|
+
preserve_images: z.boolean().optional().describe("Keep image placeholders with OCR text (default true)"),
|
|
24
|
+
},
|
|
25
|
+
}, (args, extra) => {
|
|
26
|
+
const call = convertFile(args, {
|
|
27
|
+
apiKey: process.env.MD_API_KEY,
|
|
28
|
+
apiBase,
|
|
29
|
+
fetch,
|
|
30
|
+
signal: extra.signal,
|
|
31
|
+
cwd: process.cwd(),
|
|
32
|
+
userAgent: `md-mcp/${version}`,
|
|
33
|
+
});
|
|
34
|
+
active.add(call);
|
|
35
|
+
void call.finally(() => active.delete(call)).catch(() => { });
|
|
36
|
+
return call;
|
|
37
|
+
});
|
|
38
|
+
let closing = false;
|
|
39
|
+
// The client went away (stdin closed) or asked us to stop: closing the server aborts every
|
|
40
|
+
// in-flight call's signal, so their requests stop and nothing is written; wait for that cleanup, then exit.
|
|
41
|
+
async function shutdown() {
|
|
42
|
+
if (closing)
|
|
43
|
+
return;
|
|
44
|
+
closing = true;
|
|
45
|
+
await server.close();
|
|
46
|
+
await Promise.allSettled([...active]);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
process.stdin.on("end", shutdown);
|
|
50
|
+
process.on("SIGINT", shutdown);
|
|
51
|
+
process.on("SIGTERM", shutdown);
|
|
52
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlelabs/md-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for md. by TLE Labs: convert local PDF, DOCX, PPTX, XLSX and HTML files to Markdown",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"md-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://md.tlelabs.com/docs/mcp/",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"markdown",
|
|
20
|
+
"pdf",
|
|
21
|
+
"docx",
|
|
22
|
+
"pptx",
|
|
23
|
+
"xlsx",
|
|
24
|
+
"converter"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "tsc -p tsconfig.build.json && vitest run",
|
|
30
|
+
"prepublishOnly": "npm run build && npm test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
34
|
+
"zod": "^4.6.7"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.19.43",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^4.1.12"
|
|
40
|
+
}
|
|
41
|
+
}
|