@nstdata-ai/mcp-server 0.0.0-stage → 1.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 1.0.2
4
+
5
+ - Preserve bounded page summaries and storage references when a single crawl result row exceeds the output limit; separate continuation cursors from unread page results.
6
+ - Describe reference inputs, offset units, page bounds and cursor recovery in tool schemas.
7
+ - Default Proxy checks to `https://example.com/`, with an optional target override and no requests during startup.
8
+ - Add credential-free `--help` and `--version` commands; reject unsupported arguments without printing their values.
9
+
10
+ ## 1.0.1
11
+
12
+ - Provide six Crawl tools and two HTTP/HTTPS Proxy tools in one local stdio server, with independently enabled modules.
13
+ - Support sync/async single-page scraping, bounded site submission, task status, cursor pages and Unicode/binary reference chunks.
14
+ - Prefer storage references over inline content; encode inline binary content explicitly and preserve service identifiers, empty content and truncation flags.
15
+ - Validate inputs before submission; bound deadlines, downloads and output, propagate cancellation and close active connections at shutdown.
16
+ - Reject dot-only task IDs in tool inputs and asynchronous submission acknowledgements.
17
+ - Refuse redirects and automatic retries; distinguish failed tasks from unknown submission outcomes and preserve bounded recovery identifiers.
18
+ - Filter configured credentials and withhold sensitive opaque/binary output; reject malformed UTF-8 text without changing binary bytes.
19
+ - Reuse Crawl SDK 2.0.0 and proxy-checker 1.2.0 transport with locked archive integrity; preserve the target Host header for HTTPS requests.
20
+ - Recognize successful Nstdata client-IP envelopes in Proxy checks without additional requests.
21
+ - Identify invalid environment settings by variable name without exposing their values; keep programmatic configuration errors keyed by field name.
22
+ - Include bilingual setup, tool/workflow references, executable client templates and installed CLI/type/behavior checks.
23
+ - Provide manually dispatched npm releases with environment approval, same-archive verification and public installation checks.
@@ -0,0 +1,38 @@
1
+ # Contributing
2
+
3
+ Use Node.js 22.18+ in the 22.x line, or Node.js 24. Proxy certificate tests also require `openssl` on PATH. Reuse the Crawl SDK for product API calls; reuse `@nstdata-ai/proxy-checker/transport` for Proxy requests. Keep MCP output filtering and tool schemas in this adapter.
4
+
5
+ ```sh
6
+ npm ci --ignore-scripts --no-audit --no-fund
7
+ npm run typecheck
8
+ npm test
9
+ npm run check:package
10
+ ```
11
+
12
+ For offline development, cache the complete locked dependency graph before running `npm ci --offline`. `NSTDATA_SDK_TARBALL` and `NSTDATA_CHECKER_TARBALL` can supply local SDK and transport archives to package checks; their integrity must match the lock. These inputs do not replace source dependency installation. Tests use synthetic or loopback services, not paid endpoints.
13
+
14
+ ## Local package installation
15
+
16
+ After the checks pass, run `npm pack` to build `nstdata-ai-mcp-server-1.0.2.tgz`. Install it from a separate application directory:
17
+
18
+ ```sh
19
+ npm install --ignore-scripts /path/to/nstdata-ai-mcp-server-1.0.2.tgz
20
+ ```
21
+
22
+ Configure the installed package using the README template; do not point clients at source files.
23
+
24
+ Cover observable tool inputs/results, failure/empty content, deadlines, cancellation, output limits, reference continuation and process cleanup. Keep stdout exclusively for MCP protocol traffic. Task creation must not gain automatic retries.
25
+
26
+ Update both READMEs and both language versions of the relevant reference document when public behavior changes. Keep credentials and raw private responses out of examples, fixtures and errors. Package checks install the locked graph outside the source tree, then install the tarball without modifying that lock, verify dependency versions/integrities and validate installed tools, types, configuration templates, packaged links and documented workflows. Runtime and protocol tests also run against the installed CLI, including lifecycle and interrupted-request cases. Release and documentation-check tests run in the source suite. Markdown is actually parsed, and documented npm installation and npx startup run through an isolated loopback registry. Startup checks only initialize and discover tools; they must make no business requests. Third-party registry fixtures are repacked from the already verified installed runtime graph; this does not establish public registry availability. Checks require the complete cache prepared by source installation.
27
+
28
+ Verify license ownership, dependency availability, compatibility and maintainer authorization before publishing.
29
+
30
+ ## Releases
31
+
32
+ The `Publish npm` workflow runs only when manually dispatched from `main`, with the stable version in `package.json` and a full reviewed commit SHA. Checks produce one archive and its integrity manifest. The `npm-production` reviewer approves publication of those exact bytes. The workflow finishes when `npm publish` succeeds. Public availability may be delayed; registry integrity, installation and tool discovery are checked separately.
33
+
34
+ Crawl SDK and Proxy Checker dependencies must be publicly available with the lock's exact integrity before remote CI or release. Configure [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) for this repository, `release.yml` and `npm-production`, permitting `npm publish`. Require an environment reviewer, allow only `main` and disable administrator bypass. No npm token is needed.
35
+
36
+ A new npm package must exist before trusted publishing is configured. The scope owner publishes the first reviewed archive using their own npm login and two-factor authentication; use OIDC for subsequent real versions. Configure a new binding close to that release so its first-use deadline is met. Do not create placeholder packages.
37
+
38
+ If publication fails or the outcome is uncertain, inspect the registry version and integrity before taking further action. Do not retry blindly or modify a published version.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nstdata-ai
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,61 @@
1
- # Temporary Holding Version
1
+ # Nstdata MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [中文](README_zh.md)
4
+
5
+ Use Nstdata Crawl and HTTP/HTTPS proxies from an MCP client. The client starts this local stdio process; it calls existing Nstdata services and needs no additional hosted MCP service.
6
+
7
+ ## Choose a workflow
8
+
9
+ | Your task | Guide |
10
+ |---|---|
11
+ | Retrieve one webpage | [Single page](docs/mcp.md#single-page) |
12
+ | Crawl bounded website pages | [Site crawl and pagination](docs/mcp.md#site-crawl) |
13
+ | Read referenced text or binary content | [Reference reading](docs/mcp.md#read-a-reference) |
14
+ | Request a target through a proxy | [Proxy setup and tools](docs/proxy.md) |
15
+ | Configure limits or investigate a failure | [Configuration](docs/mcp.md#configuration) · [Results and errors](docs/mcp.md#results-and-errors) |
16
+
17
+ ## Install
18
+
19
+ Supports Node.js 22 (22.18 or later) and 24. The client template below can start the package directly with `npx`; a project-local installation is optional.
20
+
21
+ ```sh
22
+ npm install @nstdata-ai/mcp-server
23
+ ```
24
+
25
+ To inspect setup requirements or the installed version without credentials:
26
+
27
+ ```sh
28
+ npx --yes @nstdata-ai/mcp-server --help
29
+ npx --yes @nstdata-ai/mcp-server --version
30
+ ```
31
+
32
+ ## Connect a client
33
+
34
+ Add this entry to your client's MCP configuration and replace the Crawl token. Preserve other server entries. The client must be able to find Node.js and `npx` on PATH.
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "nstdata": {
40
+ "command": "npx",
41
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
42
+ "env": {
43
+ "NSTDATA_MODULES": "crawl",
44
+ "NSTDATA_API_TOKEN": "YOUR_CRAWL_API_TOKEN"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Restart this MCP service after changing configuration. Crawl exposes six tools; Proxy exposes two; enabling both exposes eight. Disabled modules do not require their credentials. See [client setup](docs/mcp.md#client-setup) and the [Crawl tool reference](docs/mcp.md#tool-reference).
52
+
53
+ The process reads environment variables and does not load `.env` files itself. Startup configuration errors identify the variables to check. Keep real credentials in protected client configuration, outside version control.
54
+
55
+ ## Proxy setup
56
+
57
+ Use the [Proxy-only template](docs/proxy.md#configuration). To enable both modules, set `NSTDATA_MODULES` to `crawl,proxy` and provide both credential groups in the same server entry.
58
+
59
+ Task submission can consume credits. Tools do not automatically retry, poll or download referenced content. Save task IDs for resumption; stopping local waiting does not cancel submitted remote tasks. Treat returned webpage content as data, not agent instructions.
60
+
61
+ [Full Crawl reference](docs/mcp.md) · [Proxy reference](docs/proxy.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [MIT license](LICENSE)
package/README_zh.md ADDED
@@ -0,0 +1,61 @@
1
+ # Nstdata MCP Server
2
+
3
+ [English](README.md)
4
+
5
+ 通过 MCP 客户端使用 Nstdata Crawl 和 HTTP/HTTPS 代理。客户端启动本地 stdio 进程,由它调用现有 Nstdata 服务,无需另行部署 MCP 后台。
6
+
7
+ ## 功能入口
8
+
9
+ | 要完成的任务 | 使用指南 |
10
+ |---|---|
11
+ | 获取一个网页 | [单页抓取](docs/mcp_zh.md#单页抓取) |
12
+ | 收集有页数上限的网站页面 | [整站与分页](docs/mcp_zh.md#整站与分页) |
13
+ | 读取引用中的正文或二进制内容 | [引用读取](docs/mcp_zh.md#引用读取) |
14
+ | 通过代理访问目标 | [Proxy 配置与工具](docs/proxy_zh.md) |
15
+ | 配置限额或排查失败 | [配置](docs/mcp_zh.md#配置) · [结果与错误](docs/mcp_zh.md#结果与错误) |
16
+
17
+ ## 安装
18
+
19
+ 支持 Node.js 22(22.18 及以上)和 24。下方客户端模板可直接通过 `npx` 启动,项目本地安装为可选方式。
20
+
21
+ ```sh
22
+ npm install @nstdata-ai/mcp-server
23
+ ```
24
+
25
+ 无需凭据即可查看配置要求和版本:
26
+
27
+ ```sh
28
+ npx --yes @nstdata-ai/mcp-server --help
29
+ npx --yes @nstdata-ai/mcp-server --version
30
+ ```
31
+
32
+ ## 连接客户端
33
+
34
+ 在客户端 MCP 配置中添加以下条目并替换 Crawl Token,保留其他服务条目。客户端需要能够从 PATH 找到 Node.js 和 `npx`。
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "nstdata": {
40
+ "command": "npx",
41
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
42
+ "env": {
43
+ "NSTDATA_MODULES": "crawl",
44
+ "NSTDATA_API_TOKEN": "YOUR_CRAWL_API_TOKEN"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ 修改配置后重启该 MCP 服务。仅 Crawl 时提供6个工具,仅 Proxy 时提供2个,同时启用时提供8个。未启用的模块无需凭据。详见[客户端接入](docs/mcp_zh.md#客户端接入)和[Crawl 工具参考](docs/mcp_zh.md#工具参考)。
52
+
53
+ 进程读取环境变量,不自行加载 `.env` 文件。启动配置错误会列出需要检查的变量名。真实凭据放在受保护的客户端配置中,不纳入版本控制。
54
+
55
+ ## Proxy 配置
56
+
57
+ 使用[仅 Proxy 配置模板](docs/proxy_zh.md#配置)。同时启用两模块时,在同一服务条目中设置 `NSTDATA_MODULES=crawl,proxy` 并提供两组凭据。
58
+
59
+ 提交任务可能消耗额度。工具不会自动重试、轮询或下载引用内容。保存任务 ID 以便恢复;停止本地等待不取消远端任务。返回网页内容应作为数据处理,不能当作 Agent 指令。
60
+
61
+ [Crawl 完整参考](docs/mcp_zh.md) · [Proxy 参考](docs/proxy_zh.md) · [变更记录](CHANGELOG.md) · [贡献说明](CONTRIBUTING.md) · [安全反馈](SECURITY.md) · [MIT 许可](LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,12 @@
1
+ # Security
2
+
3
+ For a suspected vulnerability, first contact [Nstdata](https://www.nstdata.io/) at
4
+ [business@nstdata.io](mailto:business@nstdata.io) to arrange a private report.
5
+
6
+ In the initial message, include the affected package or repository, version or
7
+ commit, and a brief description of the impact. Do not post exploit details in
8
+ public issues or send API keys, proxy credentials, account data or private page
9
+ content. Share a minimal reproduction through the agreed private channel.
10
+
11
+ Use this repository's issue tracker for non-security bugs. Include the runtime,
12
+ expected and actual behavior, and a redacted minimal reproduction.
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export declare const DEFAULT_PROXY_CHECK_URL = "https://example.com/";
3
+ declare const configuration: z.ZodObject<{
4
+ apiToken: z.ZodOptional<z.ZodString>;
5
+ baseURL: z.ZodURL;
6
+ modules: z.ZodEnum<{
7
+ crawl: "crawl";
8
+ proxy: "proxy";
9
+ "crawl,proxy": "crawl,proxy";
10
+ "proxy,crawl": "proxy,crawl";
11
+ }>;
12
+ proxyURL: z.ZodOptional<z.ZodURL>;
13
+ proxyCheckURL: z.ZodOptional<z.ZodURL>;
14
+ proxyCAFile: z.ZodOptional<z.ZodString>;
15
+ httpTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
16
+ maxResponseBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
17
+ maxOutputChars: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
18
+ maxCrawlPages: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
19
+ }, z.core.$strict>;
20
+ export type Config = z.infer<typeof configuration>;
21
+ export declare class EnvironmentConfigError extends Error {
22
+ constructor(fields: string[]);
23
+ }
24
+ export declare function validateConfig(input: unknown): Config;
25
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
26
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+ import { isAbsolute } from 'node:path';
3
+ export const DEFAULT_PROXY_CHECK_URL = 'https://example.com/';
4
+ const configuration = z.strictObject({
5
+ apiToken: z.string().trim().min(1).optional(),
6
+ baseURL: z.url().refine(value => {
7
+ try {
8
+ const url = new URL(value);
9
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }),
15
+ modules: z.enum(['crawl', 'proxy', 'crawl,proxy', 'proxy,crawl']),
16
+ proxyURL: z.url().max(8192).refine(value => {
17
+ try {
18
+ const url = new URL(value);
19
+ decodeURIComponent(url.username);
20
+ decodeURIComponent(url.password);
21
+ return ['http:', 'https:'].includes(url.protocol) && url.pathname === '/' && !url.search && !url.hash;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }).optional(),
27
+ proxyCheckURL: z.url().max(8192).refine(value => {
28
+ try {
29
+ const url = new URL(value);
30
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }).optional(),
36
+ proxyCAFile: z.string().min(1).max(4096).refine(isAbsolute).optional(),
37
+ httpTimeoutMs: z.coerce.number().int().min(1).max(600000).default(120000),
38
+ maxResponseBytes: z.coerce.number().int().min(256).max(67108864).default(8388608),
39
+ maxOutputChars: z.coerce.number().int().min(1024).max(64000).default(16000),
40
+ maxCrawlPages: z.coerce.number().int().min(1).max(1000).default(10),
41
+ }).superRefine((config, ctx) => {
42
+ if (config.modules.includes('crawl') && !config.apiToken)
43
+ ctx.addIssue({ code: 'custom', path: ['apiToken'], message: 'Required for crawl' });
44
+ if (config.modules.includes('proxy')) {
45
+ if (!config.proxyURL)
46
+ ctx.addIssue({ code: 'custom', path: ['proxyURL'], message: 'Required for proxy' });
47
+ }
48
+ });
49
+ const environmentVariables = {
50
+ apiToken: 'NSTDATA_API_TOKEN',
51
+ baseURL: 'NSTDATA_API_BASE_URL',
52
+ modules: 'NSTDATA_MODULES',
53
+ proxyURL: 'NSTDATA_PROXY_URL',
54
+ proxyCheckURL: 'NSTDATA_PROXY_CHECK_URL',
55
+ proxyCAFile: 'NSTDATA_PROXY_CA_FILE',
56
+ httpTimeoutMs: 'NSTDATA_HTTP_TIMEOUT_MS',
57
+ maxResponseBytes: 'NSTDATA_MAX_RESPONSE_BYTES',
58
+ maxOutputChars: 'NSTDATA_MAX_OUTPUT_CHARS',
59
+ maxCrawlPages: 'NSTDATA_MAX_CRAWL_PAGES',
60
+ };
61
+ // Only this error's allowlisted variable names are safe for CLI diagnostics.
62
+ export class EnvironmentConfigError extends Error {
63
+ constructor(fields) {
64
+ super(`Invalid configuration: ${fields.map(field => environmentVariables[field] ?? 'environment').join(', ')}`);
65
+ }
66
+ }
67
+ function parseConfig(input, fromEnvironment) {
68
+ const result = configuration.safeParse(input);
69
+ if (!result.success) {
70
+ const fields = [...new Set(result.error.issues.map(issue => issue.path.join('.')))];
71
+ if (fromEnvironment)
72
+ throw new EnvironmentConfigError(fields);
73
+ throw new Error(`Invalid configuration: ${fields.join(', ')}`);
74
+ }
75
+ return { ...result.data, proxyCheckURL: result.data.proxyCheckURL ?? DEFAULT_PROXY_CHECK_URL };
76
+ }
77
+ export function validateConfig(input) {
78
+ return parseConfig(input, false);
79
+ }
80
+ export function loadConfig(env = process.env) {
81
+ const modules = env.NSTDATA_MODULES ?? 'crawl';
82
+ return parseConfig({
83
+ apiToken: modules.includes('crawl') ? env.NSTDATA_API_TOKEN : undefined,
84
+ baseURL: modules.includes('crawl') ? env.NSTDATA_API_BASE_URL ?? 'https://api.nstdata.io' : 'https://api.nstdata.io',
85
+ modules,
86
+ proxyURL: modules.includes('proxy') ? env.NSTDATA_PROXY_URL : undefined,
87
+ proxyCheckURL: modules.includes('proxy') ? env.NSTDATA_PROXY_CHECK_URL : undefined,
88
+ proxyCAFile: modules.includes('proxy') ? env.NSTDATA_PROXY_CA_FILE : undefined,
89
+ httpTimeoutMs: env.NSTDATA_HTTP_TIMEOUT_MS,
90
+ maxResponseBytes: env.NSTDATA_MAX_RESPONSE_BYTES,
91
+ maxOutputChars: env.NSTDATA_MAX_OUTPUT_CHARS,
92
+ maxCrawlPages: modules.includes('crawl') ? env.NSTDATA_MAX_CRAWL_PAGES : undefined,
93
+ }, true);
94
+ }
@@ -0,0 +1,12 @@
1
+ import { type ScrapeDataDto, type RequestOptions } from '@nstdata-ai/crawl';
2
+ export declare function taskFailed(result: {
3
+ status?: string;
4
+ success?: boolean;
5
+ }): boolean;
6
+ export declare function requireSubmissionId(result: {
7
+ id?: string;
8
+ status?: string;
9
+ success?: boolean;
10
+ }): void;
11
+ /** References stay lazy; only read_result downloads storage content. */
12
+ export declare function serializeData(data: ScrapeDataDto | null | undefined, options: RequestOptions): Promise<Record<string, unknown> | undefined>;
@@ -0,0 +1,28 @@
1
+ import { NstDataApiError } from '@nstdata-ai/crawl';
2
+ import { binaryContent } from '../shared/output.js';
3
+ import { identifier } from './crawl-schema.js';
4
+ export function taskFailed(result) {
5
+ return result.success === false || result.status === 'failed' || result.status === 'expired';
6
+ }
7
+ export function requireSubmissionId(result) {
8
+ if (taskFailed(result))
9
+ return;
10
+ const parsed = identifier.safeParse(result.id);
11
+ if (!parsed.success || parsed.data !== result.id)
12
+ throw new NstDataApiError('API error: unusable submission ID');
13
+ }
14
+ /** References stay lazy; only read_result downloads storage content. */
15
+ export async function serializeData(data, options) {
16
+ if (!data)
17
+ return undefined;
18
+ return {
19
+ markdown: data.markdownRef ? undefined : await data.getMarkdown(options),
20
+ html: data.htmlRef ? undefined : await data.getHtml(options),
21
+ pdf: data.pdfRef ? undefined : binaryContent(await data.getPdf(options)),
22
+ screenshot: data.screenshotRef ? undefined : binaryContent(await data.getScreenshot(options)),
23
+ rawData: data.rawDataRef ? undefined : binaryContent(await data.getRawData(options)),
24
+ links: data.links, metadata: data.metadata, truncated: data.truncated,
25
+ markdownRef: data.markdownRef, htmlRef: data.htmlRef, pdfRef: data.pdfRef,
26
+ screenshotRef: data.screenshotRef, rawDataRef: data.rawDataRef,
27
+ };
28
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ export declare const httpURL: z.ZodURL;
3
+ export declare const identifier: z.ZodString;
4
+ export declare const formats: z.ZodDefault<z.ZodArray<z.ZodEnum<{
5
+ markdown: "markdown";
6
+ html: "html";
7
+ pdf: "pdf";
8
+ screenshot: "screenshot";
9
+ rawData: "rawData";
10
+ links: "links";
11
+ }>>>;
12
+ export declare const scrapeOptions: z.ZodDefault<z.ZodObject<{
13
+ cookies: z.ZodOptional<z.ZodArray<z.ZodObject<{
14
+ domain: z.ZodOptional<z.ZodString>;
15
+ httpOnly: z.ZodOptional<z.ZodBoolean>;
16
+ name: z.ZodString;
17
+ path: z.ZodOptional<z.ZodString>;
18
+ secure: z.ZodOptional<z.ZodBoolean>;
19
+ value: z.ZodString;
20
+ }, z.core.$strict>>>;
21
+ proxy: z.ZodOptional<z.ZodObject<{
22
+ country: z.ZodString;
23
+ }, z.core.$strict>>;
24
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
25
+ excludeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ includeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ jsRender: z.ZodOptional<z.ZodObject<{
28
+ actions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ type: z.ZodLiteral<"click">;
30
+ selector: z.ZodString;
31
+ waitSelector: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strict>, z.ZodObject<{
33
+ type: z.ZodLiteral<"wait">;
34
+ milliseconds: z.ZodNumber;
35
+ }, z.core.$strict>, z.ZodObject<{
36
+ type: z.ZodLiteral<"waitSelector">;
37
+ selector: z.ZodString;
38
+ milliseconds: z.ZodOptional<z.ZodNumber>;
39
+ }, z.core.$strict>, z.ZodObject<{
40
+ type: z.ZodLiteral<"scrollX">;
41
+ x: z.ZodNumber;
42
+ text: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ type: z.ZodLiteral<"scrollY">;
45
+ y: z.ZodNumber;
46
+ text: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strict>, z.ZodObject<{
48
+ type: z.ZodLiteral<"scrollTo">;
49
+ selector: z.ZodString;
50
+ }, z.core.$strict>, z.ZodObject<{
51
+ type: z.ZodLiteral<"fill">;
52
+ selector: z.ZodString;
53
+ text: z.ZodString;
54
+ }, z.core.$strict>, z.ZodObject<{
55
+ type: z.ZodLiteral<"execute">;
56
+ execute: z.ZodString;
57
+ }, z.core.$strict>, z.ZodObject<{
58
+ type: z.ZodLiteral<"screenshot">;
59
+ fullScreenshot: z.ZodOptional<z.ZodBoolean>;
60
+ particularScreenshot: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strict>, z.ZodObject<{
62
+ type: z.ZodLiteral<"waitForRequestCompletion">;
63
+ urlPattern: z.ZodString;
64
+ milliseconds: z.ZodOptional<z.ZodNumber>;
65
+ }, z.core.$strict>], "type">>>;
66
+ waitFor: z.ZodOptional<z.ZodNumber>;
67
+ waitForSelector: z.ZodOptional<z.ZodString>;
68
+ }, z.core.$strict>>;
69
+ mobile: z.ZodOptional<z.ZodBoolean>;
70
+ onlyMainContent: z.ZodOptional<z.ZodBoolean>;
71
+ removeSelectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
72
+ targetSelector: z.ZodOptional<z.ZodString>;
73
+ timeout: z.ZodOptional<z.ZodNumber>;
74
+ }, z.core.$strict>>;
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ const text = z.string().min(1).max(8192);
3
+ const selector = text;
4
+ const milliseconds = z.number().int().min(0).max(600000);
5
+ export const httpURL = z.url().max(8192).refine(value => {
6
+ const url = new URL(value);
7
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
8
+ }, 'Use an HTTP(S) URL without embedded credentials').describe('Target HTTP(S) URL without embedded credentials. For a site crawl this is the single seed URL.');
9
+ export const identifier = z.string().trim().min(1).max(256)
10
+ .refine(value => value !== '.' && value !== '..', 'Task IDs must not be . or ..');
11
+ export const formats = z.array(z.enum(['markdown', 'html', 'links', 'pdf', 'screenshot', 'rawData'])).min(1).max(6).default(['markdown']).describe('Requested output formats. Large results may contain storage references instead of inline content.');
12
+ const action = z.discriminatedUnion('type', [
13
+ z.strictObject({ type: z.literal('click'), selector, waitSelector: selector.optional() }),
14
+ z.strictObject({ type: z.literal('wait'), milliseconds }),
15
+ z.strictObject({ type: z.literal('waitSelector'), selector, milliseconds: milliseconds.optional() }),
16
+ z.strictObject({ type: z.literal('scrollX'), x: z.number().int(), text: text.optional() }),
17
+ z.strictObject({ type: z.literal('scrollY'), y: z.number().int(), text: text.optional() }),
18
+ z.strictObject({ type: z.literal('scrollTo'), selector }),
19
+ z.strictObject({ type: z.literal('fill'), selector, text: z.string().max(8192) }),
20
+ z.strictObject({ type: z.literal('execute'), execute: text }),
21
+ z.strictObject({ type: z.literal('screenshot'), fullScreenshot: z.boolean().optional(), particularScreenshot: selector.optional() }),
22
+ z.strictObject({ type: z.literal('waitForRequestCompletion'), urlPattern: text, milliseconds: milliseconds.optional() }),
23
+ ]);
24
+ export const scrapeOptions = z.strictObject({
25
+ cookies: z.array(z.strictObject({ domain: text.optional(), httpOnly: z.boolean().optional(), name: text,
26
+ path: text.optional(), secure: z.boolean().optional(), value: z.string().max(8192) })).max(100).optional(),
27
+ proxy: z.strictObject({ country: z.string().regex(/^[a-zA-Z]{2}$/) }).optional(),
28
+ headers: z.record(z.string().min(1).max(256), z.string().max(8192)).refine(value => Object.keys(value).length <= 100).optional(),
29
+ excludeTags: z.array(text).max(100).optional(),
30
+ includeTags: z.array(text).max(100).optional(),
31
+ jsRender: z.strictObject({ actions: z.array(action).max(100).optional(), waitFor: z.number().int().min(1000).max(30000).optional(), waitForSelector: selector.optional() }).optional(),
32
+ mobile: z.boolean().optional(),
33
+ onlyMainContent: z.boolean().optional(),
34
+ removeSelectors: z.array(selector).max(100).optional(),
35
+ targetSelector: selector.optional(),
36
+ timeout: z.number().int().min(1000).max(60000).optional(),
37
+ }).default({}).describe('Optional Crawl service settings. Set onlyMainContent=false when full-page navigation is needed for link discovery.');
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { NstDataClient } from '@nstdata-ai/crawl';
3
+ import { type Config } from '../config.js';
4
+ export declare function registerCrawlTools(server: McpServer, client: NstDataClient, config: Config): void;
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ import { isUtf8 } from 'node:buffer';
3
+ import { guard, ToolFailure } from '../shared/errors.js';
4
+ import { httpURL, identifier, formats, scrapeOptions } from './crawl-schema.js';
5
+ import { serializeData, taskFailed, requireSubmissionId } from './crawl-result.js';
6
+ export function registerCrawlTools(server, client, config) {
7
+ server.registerTool('nstdata_crawl_read_result', {
8
+ description: 'Read a bounded chunk of a Crawl storage reference. Text offsets count Unicode code points; binary offsets count bytes. Each call downloads the reference again, within the response byte limit.',
9
+ inputSchema: z.strictObject({
10
+ ref: z.string().min(1).max(4096).describe('Copy the actual value of markdownRef, htmlRef, pdfRef, screenshotRef or rawDataRef into ref; do not use the output field name as an input key.'),
11
+ format: z.enum(['markdown', 'html', 'pdf', 'screenshot', 'rawData']).describe('Must match the reference field: markdownRef uses markdown, htmlRef uses html, and so on.'),
12
+ offset: z.number().int().min(0).default(0).describe('Start position: Unicode code points for markdown/html, bytes for binary formats. Use the preceding read result\'s nextOffset to continue.'),
13
+ length: z.number().int().min(1).max(4000).default(2000).describe('Maximum Unicode code points for markdown/html, or bytes for binary formats, in this chunk. Reduce it if the chunk exceeds the output limit.'),
14
+ }),
15
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
16
+ }, async (args, ctx) => guard(ctx, config, async (options) => {
17
+ const bytes = Buffer.from(await client.readStorage(args.ref, options));
18
+ const isText = args.format === 'markdown' || args.format === 'html';
19
+ if (isText && !isUtf8(bytes))
20
+ throw new ToolFailure('INVALID_RESPONSE', 'The reference is not valid UTF-8. No text was returned.');
21
+ const points = isText ? Array.from(bytes.toString('utf8')) : undefined;
22
+ const total = points?.length ?? bytes.length;
23
+ const end = Math.min(args.offset + args.length, total);
24
+ const output = { format: args.format, encoding: isText ? 'utf-8' : 'base64', unit: isText ? 'codePoints' : 'bytes', offset: args.offset,
25
+ ...(points ? { text: points.slice(args.offset, end).join('') } : { base64: bytes.subarray(args.offset, end).toString('base64') }),
26
+ total, nextOffset: end < total ? end : undefined };
27
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
28
+ }));
29
+ server.registerTool('nstdata_crawl_start', {
30
+ description: 'Start a bounded site crawl and return its task ID immediately.',
31
+ inputSchema: z.strictObject({
32
+ url: httpURL, formats, options: scrapeOptions,
33
+ maxPages: z.number().int().min(1).max(config.maxCrawlPages).default(config.maxCrawlPages).describe('Page-count upper bound, not a guarantee of that many pages. Check actual returned URLs and per-page results.'),
34
+ maxDepth: z.number().int().min(0).max(100).optional().describe('Optional service-side link-discovery depth limit; depth counting follows the Crawl service.'),
35
+ includeUrls: z.array(z.string().max(8192)).max(100).optional().describe('Optional service-side include filters for discovered links. These are not extra seed URLs or a batch scrape list.'),
36
+ excludeUrls: z.array(z.string().max(8192)).max(100).optional().describe('Optional service-side exclude filters for discovered links.'),
37
+ ignoreQuery: z.boolean().optional().describe('Optional service-side query-string handling setting.'),
38
+ }),
39
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
40
+ }, async (args, ctx) => guard(ctx, config, async (options) => {
41
+ const { options: advanced, ...request } = args;
42
+ const output = await client.submitCrawlTask({ ...request, ...advanced }, options);
43
+ requireSubmissionId(output);
44
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: { ...output }, isError: taskFailed(output) };
45
+ }, 'task'));
46
+ server.registerTool('nstdata_crawl_status', {
47
+ description: 'Read aggregate crawl progress without polling.',
48
+ inputSchema: z.strictObject({ crawlId: identifier.describe('Actual id returned by nstdata_crawl_start; reuse the existing task instead of submitting another crawl.') }),
49
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
50
+ }, async (args, ctx) => guard(ctx, config, async (options) => {
51
+ const output = { ...await client.getCrawlStatus(args.crawlId, options), crawlId: args.crawlId };
52
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: { ...output }, isError: taskFailed(output) };
53
+ }));
54
+ server.registerTool('nstdata_crawl_pages', {
55
+ description: 'Read one page of crawl results without downloading referenced files. On OUTPUT_LIMIT, retry the same input cursor with a smaller limit. A single-row error may retain recovery.page references; handle that page before using recovery.continuation.',
56
+ inputSchema: z.strictObject({
57
+ crawlId: identifier.describe('Actual id of the existing site crawl.'),
58
+ afterCursor: z.string().min(1).max(8192).optional().describe('Copy nextCursor from a successful page read. On OUTPUT_LIMIT, keep the original input cursor. Only use recovery.continuation.nextCursor after handling recovery.page. Mutually exclusive with beforeCursor.'),
59
+ beforeCursor: z.string().min(1).max(8192).optional().describe('Copy prevCursor from a successful read, or recovery.continuation.prevCursor after handling recovery.page. On OUTPUT_LIMIT keep the original input cursor; do not combine with afterCursor.'),
60
+ limit: z.number().int().min(1).max(100).default(10).describe('Maximum rows in this response. Reduce down to 1 on OUTPUT_LIMIT; a single row may still exceed the output cap.'),
61
+ }).refine(args => !(args.afterCursor && args.beforeCursor)),
62
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
63
+ }, async ({ crawlId, ...pageOptions }, ctx) => guard(ctx, config, async (options) => {
64
+ const result = await client.listCrawlPages(crawlId, pageOptions, options);
65
+ const output = { ...result, crawlId, data: result.data && await Promise.all(result.data.map(async (page) => ({
66
+ ...page, data: await serializeData(page.data, options),
67
+ }))) };
68
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
69
+ }));
70
+ server.registerTool('nstdata_crawl_scrape', {
71
+ description: 'Retrieve a webpage through Nstdata Crawl. Creates a potentially billable scrape task.',
72
+ inputSchema: z.strictObject({
73
+ url: httpURL, formats, options: scrapeOptions,
74
+ mode: z.enum(['sync', 'async']).default('sync').describe('sync waits for the single-page response; async returns a task id to resume with nstdata_crawl_scrape_result. Neither mode retries submission.'),
75
+ }),
76
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
77
+ }, async (args, ctx) => guard(ctx, config, async (options) => {
78
+ const { mode, options: advanced, ...common } = args;
79
+ const request = { ...common, ...advanced };
80
+ if (mode === 'async') {
81
+ const submitted = await client.submitScrapeTask(request, options);
82
+ requireSubmissionId(submitted);
83
+ return { content: [{ type: 'text', text: JSON.stringify(submitted) }], structuredContent: { ...submitted }, isError: taskFailed(submitted) };
84
+ }
85
+ const result = await client.submitScrapeTaskSync(request, options);
86
+ const output = { id: result.id, status: result.status, success: result.success, errorCode: result.errorCode, errorMessage: result.errorMessage, data: await serializeData(result.data, options) };
87
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output, isError: taskFailed(result) };
88
+ }, 'task'));
89
+ server.registerTool('nstdata_crawl_scrape_result', {
90
+ description: 'Resume a submitted single-page task. Processing is not completion; a failed or expired task is reported explicitly.',
91
+ inputSchema: z.strictObject({ taskId: identifier.describe('Actual id returned by an existing single-page submission; not a site crawl id or a storage reference.') }),
92
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
93
+ }, async (args, ctx) => guard(ctx, config, async (options) => {
94
+ const result = await client.getScrapeTaskResult(args.taskId, options);
95
+ const output = { id: result.id, taskId: args.taskId, status: result.status, success: result.success,
96
+ errorCode: result.errorCode, errorMessage: result.errorMessage,
97
+ data: await serializeData(result.data, options),
98
+ };
99
+ return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output,
100
+ isError: taskFailed(result) };
101
+ }));
102
+ }
@@ -0,0 +1,3 @@
1
+ import { type McpServer } from '@modelcontextprotocol/server';
2
+ import { type Config } from '../config.js';
3
+ export declare function registerProxyTools(server: McpServer, config: Config): void;