@nstdata-ai/mcp-server 1.0.1 → 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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
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
+
3
10
  ## 1.0.1
4
11
 
5
12
  - Provide six Crawl tools and two HTTP/HTTPS Proxy tools in one local stdio server, with independently enabled modules.
package/CONTRIBUTING.md CHANGED
@@ -13,10 +13,10 @@ For offline development, cache the complete locked dependency graph before runni
13
13
 
14
14
  ## Local package installation
15
15
 
16
- After the checks pass, run `npm pack` to build `nstdata-ai-mcp-server-1.0.1.tgz`. Install it from a separate application directory:
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
17
 
18
18
  ```sh
19
- npm install --ignore-scripts /path/to/nstdata-ai-mcp-server-1.0.1.tgz
19
+ npm install --ignore-scripts /path/to/nstdata-ai-mcp-server-1.0.2.tgz
20
20
  ```
21
21
 
22
22
  Configure the installed package using the README template; do not point clients at source files.
@@ -29,7 +29,7 @@ Verify license ownership, dependency availability, compatibility and maintainer
29
29
 
30
30
  ## Releases
31
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. Published integrity, default installation and tool discovery must pass afterward.
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
33
 
34
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
35
 
package/README.md CHANGED
@@ -22,6 +22,13 @@ Supports Node.js 22 (22.18 or later) and 24. The client template below can start
22
22
  npm install @nstdata-ai/mcp-server
23
23
  ```
24
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
+
25
32
  ## Connect a client
26
33
 
27
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.
@@ -31,7 +38,7 @@ Add this entry to your client's MCP configuration and replace the Crawl token. P
31
38
  "mcpServers": {
32
39
  "nstdata": {
33
40
  "command": "npx",
34
- "args": ["--yes", "@nstdata-ai/mcp-server@1.0.1"],
41
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
35
42
  "env": {
36
43
  "NSTDATA_MODULES": "crawl",
37
44
  "NSTDATA_API_TOKEN": "YOUR_CRAWL_API_TOKEN"
package/README_zh.md CHANGED
@@ -22,6 +22,13 @@
22
22
  npm install @nstdata-ai/mcp-server
23
23
  ```
24
24
 
25
+ 无需凭据即可查看配置要求和版本:
26
+
27
+ ```sh
28
+ npx --yes @nstdata-ai/mcp-server --help
29
+ npx --yes @nstdata-ai/mcp-server --version
30
+ ```
31
+
25
32
  ## 连接客户端
26
33
 
27
34
  在客户端 MCP 配置中添加以下条目并替换 Crawl Token,保留其他服务条目。客户端需要能够从 PATH 找到 Node.js 和 `npx`。
@@ -31,7 +38,7 @@ npm install @nstdata-ai/mcp-server
31
38
  "mcpServers": {
32
39
  "nstdata": {
33
40
  "command": "npx",
34
- "args": ["--yes", "@nstdata-ai/mcp-server@1.0.1"],
41
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
35
42
  "env": {
36
43
  "NSTDATA_MODULES": "crawl",
37
44
  "NSTDATA_API_TOKEN": "YOUR_CRAWL_API_TOKEN"
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ export declare const DEFAULT_PROXY_CHECK_URL = "https://example.com/";
2
3
  declare const configuration: z.ZodObject<{
3
4
  apiToken: z.ZodOptional<z.ZodString>;
4
5
  baseURL: z.ZodURL;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { isAbsolute } from 'node:path';
3
+ export const DEFAULT_PROXY_CHECK_URL = 'https://example.com/';
3
4
  const configuration = z.strictObject({
4
5
  apiToken: z.string().trim().min(1).optional(),
5
6
  baseURL: z.url().refine(value => {
@@ -43,8 +44,6 @@ const configuration = z.strictObject({
43
44
  if (config.modules.includes('proxy')) {
44
45
  if (!config.proxyURL)
45
46
  ctx.addIssue({ code: 'custom', path: ['proxyURL'], message: 'Required for proxy' });
46
- if (!config.proxyCheckURL)
47
- ctx.addIssue({ code: 'custom', path: ['proxyCheckURL'], message: 'Required for proxy' });
48
47
  }
49
48
  });
50
49
  const environmentVariables = {
@@ -73,7 +72,7 @@ function parseConfig(input, fromEnvironment) {
73
72
  throw new EnvironmentConfigError(fields);
74
73
  throw new Error(`Invalid configuration: ${fields.join(', ')}`);
75
74
  }
76
- return result.data;
75
+ return { ...result.data, proxyCheckURL: result.data.proxyCheckURL ?? DEFAULT_PROXY_CHECK_URL };
77
76
  }
78
77
  export function validateConfig(input) {
79
78
  return parseConfig(input, false);
@@ -5,10 +5,10 @@ const milliseconds = z.number().int().min(0).max(600000);
5
5
  export const httpURL = z.url().max(8192).refine(value => {
6
6
  const url = new URL(value);
7
7
  return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
8
- }, 'Use an HTTP(S) URL without embedded credentials');
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
9
  export const identifier = z.string().trim().min(1).max(256)
10
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']);
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
12
  const action = z.discriminatedUnion('type', [
13
13
  z.strictObject({ type: z.literal('click'), selector, waitSelector: selector.optional() }),
14
14
  z.strictObject({ type: z.literal('wait'), milliseconds }),
@@ -34,4 +34,4 @@ export const scrapeOptions = z.strictObject({
34
34
  removeSelectors: z.array(selector).max(100).optional(),
35
35
  targetSelector: selector.optional(),
36
36
  timeout: z.number().int().min(1000).max(60000).optional(),
37
- }).default({});
37
+ }).default({}).describe('Optional Crawl service settings. Set onlyMainContent=false when full-page navigation is needed for link discovery.');
@@ -6,7 +6,12 @@ import { serializeData, taskFailed, requireSubmissionId } from './crawl-result.j
6
6
  export function registerCrawlTools(server, client, config) {
7
7
  server.registerTool('nstdata_crawl_read_result', {
8
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({ ref: z.string().min(1).max(4096), format: z.enum(['markdown', 'html', 'pdf', 'screenshot', 'rawData']), offset: z.number().int().min(0).default(0), length: z.number().int().min(1).max(4000).default(2000) }),
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
+ }),
10
15
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
11
16
  }, async (args, ctx) => guard(ctx, config, async (options) => {
12
17
  const bytes = Buffer.from(await client.readStorage(args.ref, options));
@@ -23,7 +28,14 @@ export function registerCrawlTools(server, client, config) {
23
28
  }));
24
29
  server.registerTool('nstdata_crawl_start', {
25
30
  description: 'Start a bounded site crawl and return its task ID immediately.',
26
- inputSchema: z.strictObject({ url: httpURL, formats, options: scrapeOptions, maxPages: z.number().int().min(1).max(config.maxCrawlPages).default(config.maxCrawlPages), maxDepth: z.number().int().min(0).max(100).optional(), includeUrls: z.array(z.string().max(8192)).max(100).optional(), excludeUrls: z.array(z.string().max(8192)).max(100).optional(), ignoreQuery: z.boolean().optional() }),
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
+ }),
27
39
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
28
40
  }, async (args, ctx) => guard(ctx, config, async (options) => {
29
41
  const { options: advanced, ...request } = args;
@@ -33,15 +45,20 @@ export function registerCrawlTools(server, client, config) {
33
45
  }, 'task'));
34
46
  server.registerTool('nstdata_crawl_status', {
35
47
  description: 'Read aggregate crawl progress without polling.',
36
- inputSchema: z.strictObject({ crawlId: identifier }),
48
+ inputSchema: z.strictObject({ crawlId: identifier.describe('Actual id returned by nstdata_crawl_start; reuse the existing task instead of submitting another crawl.') }),
37
49
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
38
50
  }, async (args, ctx) => guard(ctx, config, async (options) => {
39
51
  const output = { ...await client.getCrawlStatus(args.crawlId, options), crawlId: args.crawlId };
40
52
  return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: { ...output }, isError: taskFailed(output) };
41
53
  }));
42
54
  server.registerTool('nstdata_crawl_pages', {
43
- description: 'Read one page of crawl results and return actual cursors and storage references. Does not download referenced files.',
44
- inputSchema: z.strictObject({ crawlId: identifier, afterCursor: z.string().min(1).max(8192).optional(), beforeCursor: z.string().min(1).max(8192).optional(), limit: z.number().int().min(1).max(100).default(10) }).refine(args => !(args.afterCursor && args.beforeCursor)),
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)),
45
62
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
46
63
  }, async ({ crawlId, ...pageOptions }, ctx) => guard(ctx, config, async (options) => {
47
64
  const result = await client.listCrawlPages(crawlId, pageOptions, options);
@@ -54,7 +71,7 @@ export function registerCrawlTools(server, client, config) {
54
71
  description: 'Retrieve a webpage through Nstdata Crawl. Creates a potentially billable scrape task.',
55
72
  inputSchema: z.strictObject({
56
73
  url: httpURL, formats, options: scrapeOptions,
57
- mode: z.enum(['sync', 'async']).default('sync'),
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.'),
58
75
  }),
59
76
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
60
77
  }, async (args, ctx) => guard(ctx, config, async (options) => {
@@ -71,7 +88,7 @@ export function registerCrawlTools(server, client, config) {
71
88
  }, 'task'));
72
89
  server.registerTool('nstdata_crawl_scrape_result', {
73
90
  description: 'Resume a submitted single-page task. Processing is not completion; a failed or expired task is reported explicitly.',
74
- inputSchema: z.strictObject({ taskId: identifier }),
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.') }),
75
92
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
76
93
  }, async (args, ctx) => guard(ctx, config, async (options) => {
77
94
  const result = await client.getScrapeTaskResult(args.taskId, options);
@@ -27,10 +27,10 @@ export function registerProxyTools(server, config) {
27
27
  server.registerTool('nstdata_proxy_fetch', {
28
28
  description: 'Read an HTTP(S) page through the configured proxy. Does not follow redirects or retry; may consume billable traffic.',
29
29
  inputSchema: z.strictObject({
30
- url: z.url().max(8192).refine(value => { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password; }),
31
- method: z.enum(['GET', 'HEAD']).default('GET'),
32
- format: z.enum(['text', 'base64']).default('text'),
33
- headers: targetHeaders.optional(),
30
+ url: z.url().max(8192).refine(value => { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password; }).describe('HTTP(S) target accessed through the configured proxy; no embedded credentials.'),
31
+ method: z.enum(['GET', 'HEAD']).default('GET').describe('GET returns content; HEAD requests response headers without a body.'),
32
+ format: z.enum(['text', 'base64']).default('text').describe('text requires UTF-8; base64 preserves binary bytes.'),
33
+ headers: targetHeaders.optional().describe('Optional target request headers. Send Authorization or Cookie only to trusted targets; proxy and connection headers are reserved.'),
34
34
  }),
35
35
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
36
36
  }, async (args, ctx) => guard(ctx, config, async (options) => {
@@ -44,7 +44,7 @@ export function registerProxyTools(server, config) {
44
44
  isError: response.statusCode < 200 || response.statusCode >= 300 };
45
45
  }));
46
46
  server.registerTool('nstdata_proxy_check', {
47
- description: 'Check the operator-configured target through the proxy, once.',
47
+ description: 'Check the proxy once against NSTDATA_PROXY_CHECK_URL, or https://example.com/ by default. Returns observedIp only when the target echoes a valid IP. Startup does not run this check.',
48
48
  inputSchema: z.strictObject({}),
49
49
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
50
50
  }, async (_args, ctx) => guard(ctx, config, async (options) => {
@@ -50,21 +50,54 @@ export function boundResult(result, config) {
50
50
  return { ...result, content: [{ type: 'text', text }], structuredContent: data };
51
51
  }
52
52
  const recovery = {};
53
- const error = { code: 'OUTPUT_LIMIT', message: 'Result exceeds the local output limit. Reduce source content or use a supported smaller page/chunk/reference. Do not automatically repeat a task submission.', recovery };
53
+ const pageList = Array.isArray(data.data);
54
+ const error = { code: 'OUTPUT_LIMIT', message: pageList
55
+ ? 'Page output exceeds the local limit. Retry with the same input cursor and a smaller limit. If recovery.page has the required references, read them before using recovery.continuation, which skips that page. Do not automatically repeat a task submission.'
56
+ : 'Result exceeds the local output limit. Reduce source content or use a supported smaller page/chunk/reference. Do not automatically repeat a task submission.', recovery };
54
57
  const add = (key, value) => {
55
- if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
56
- return;
58
+ if (value === undefined)
59
+ return false;
57
60
  const candidate = { ...recovery, [key]: value };
58
- if (JSON.stringify({ error: { ...error, recovery: candidate } }).length <= config.maxOutputChars)
59
- Object.assign(recovery, candidate);
61
+ if (JSON.stringify({ error: { ...error, recovery: candidate } }).length > config.maxOutputChars)
62
+ return false;
63
+ Object.assign(recovery, candidate);
64
+ return true;
60
65
  };
61
- for (const key of ['statusCode', 'id', 'taskId', 'crawlId', 'ref', 'format', 'offset', 'nextCursor', 'prevCursor'])
62
- add(key, data[key]);
63
- if (data.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
66
+ const scalar = (value) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
67
+ for (const key of ['statusCode', 'id', 'taskId', 'crawlId', 'ref', 'format', 'offset']) {
68
+ if (scalar(data[key]))
69
+ add(key, data[key]);
70
+ }
71
+ if (Array.isArray(data.data)) {
72
+ if (data.data.length === 1 && data.data[0] && typeof data.data[0] === 'object') {
73
+ const page = data.data[0];
74
+ const summary = {};
75
+ for (const key of ['id', 'taskId', 'url', 'status', 'success', 'errorCode']) {
76
+ if (scalar(page[key]) && add('page', { ...summary, [key]: page[key] }))
77
+ summary[key] = page[key];
78
+ }
79
+ const content = page.data;
80
+ const references = {};
81
+ for (const key of ['markdownRef', 'htmlRef', 'pdfRef', 'screenshotRef', 'rawDataRef']) {
82
+ if (typeof content?.[key] === 'string' && content[key] && add('page', { ...summary, data: { ...references, [key]: content[key] } }))
83
+ references[key] = content[key];
84
+ }
85
+ if (Object.keys(references).length) {
86
+ if (typeof content?.truncated === 'boolean')
87
+ add('page', { ...summary, data: { ...references, truncated: content.truncated } });
88
+ const continuation = Object.fromEntries(['nextCursor', 'prevCursor']
89
+ .filter(key => typeof data[key] === 'string' && data[key]).map(key => [key, data[key]]));
90
+ if (Object.keys(continuation).length)
91
+ add('continuation', continuation);
92
+ }
93
+ }
94
+ }
95
+ else if (data.data && typeof data.data === 'object') {
64
96
  const page = data.data;
65
- for (const key of ['markdownRef', 'htmlRef', 'pdfRef', 'screenshotRef', 'rawDataRef'])
66
- add(key, page[key]);
67
- add('truncated', page.truncated);
97
+ for (const key of ['markdownRef', 'htmlRef', 'pdfRef', 'screenshotRef', 'rawDataRef', 'truncated']) {
98
+ if (scalar(page[key]))
99
+ add(key, page[key]);
100
+ }
68
101
  }
69
102
  const output = { error };
70
103
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
package/dist/stdio.js CHANGED
@@ -1,35 +1,59 @@
1
1
  #!/usr/bin/env node
2
2
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
3
3
  import { ProxyTransportError } from '@nstdata-ai/proxy-checker/transport';
4
+ import { createRequire } from 'node:module';
4
5
  import { createServer, loadConfig } from './server.js';
5
- import { EnvironmentConfigError } from './config.js';
6
- try {
7
- const config = loadConfig();
8
- // Validate module resources before the lazy stdio negotiation receives input.
9
- let initialServer;
6
+ import { DEFAULT_PROXY_CHECK_URL, EnvironmentConfigError } from './config.js';
7
+ const args = process.argv.slice(2);
8
+ if (args.length === 1 && args[0] === '--help') {
9
+ console.log(`Usage: nstdata-mcp-server [--help | --version]
10
+
11
+ With no arguments, start a local stdio MCP server.
12
+ Set environment variables in your MCP client configuration:
13
+ NSTDATA_MODULES crawl (default), proxy, or crawl,proxy
14
+ NSTDATA_API_TOKEN Required when Crawl is enabled
15
+ NSTDATA_PROXY_URL Required when Proxy is enabled; HTTP(S) gateway URL
16
+ NSTDATA_PROXY_CHECK_URL Optional check target; default ${DEFAULT_PROXY_CHECK_URL}
17
+
18
+ Startup does not submit tasks or check the proxy. Calls may consume credits or traffic.
19
+ Configuration reference: https://github.com/nstdata-ai/nstdata-mcp-server#readme`);
20
+ }
21
+ else if (args.length === 1 && args[0] === '--version') {
22
+ const { version } = createRequire(import.meta.url)('../package.json');
23
+ console.log(version);
24
+ }
25
+ else if (args.length) {
26
+ console.error('Unsupported arguments. Use --help for usage.');
27
+ process.exitCode = 1;
28
+ }
29
+ else
10
30
  try {
11
- initialServer = createServer(config);
31
+ const config = loadConfig();
32
+ // Validate module resources before the lazy stdio negotiation receives input.
33
+ let initialServer;
34
+ try {
35
+ initialServer = createServer(config);
36
+ }
37
+ catch (error) {
38
+ if (config.proxyCAFile && error instanceof ProxyTransportError && error.code === 'INVALID_CONFIG') {
39
+ throw new EnvironmentConfigError(['proxyCAFile']);
40
+ }
41
+ throw error;
42
+ }
43
+ const handle = serveStdio(() => {
44
+ const server = initialServer ?? createServer(config);
45
+ initialServer = undefined;
46
+ return server;
47
+ }, {
48
+ onerror: () => { console.error('MCP protocol error.'); },
49
+ });
50
+ const shutdown = () => { void handle.close().catch(() => { process.exitCode = 1; }); };
51
+ process.once('SIGINT', shutdown);
52
+ process.once('SIGTERM', shutdown);
12
53
  }
13
54
  catch (error) {
14
- if (config.proxyCAFile && error instanceof ProxyTransportError && error.code === 'INVALID_CONFIG') {
15
- throw new EnvironmentConfigError(['proxyCAFile']);
16
- }
17
- throw error;
55
+ console.error(error instanceof EnvironmentConfigError
56
+ ? error.message
57
+ : 'Unable to start Nstdata MCP. Check the documented environment configuration.');
58
+ process.exitCode = 1;
18
59
  }
19
- const handle = serveStdio(() => {
20
- const server = initialServer ?? createServer(config);
21
- initialServer = undefined;
22
- return server;
23
- }, {
24
- onerror: () => { console.error('MCP protocol error.'); },
25
- });
26
- const shutdown = () => { void handle.close().catch(() => { process.exitCode = 1; }); };
27
- process.once('SIGINT', shutdown);
28
- process.once('SIGTERM', shutdown);
29
- }
30
- catch (error) {
31
- console.error(error instanceof EnvironmentConfigError
32
- ? error.message
33
- : 'Unable to start Nstdata MCP. Check the documented environment configuration.');
34
- process.exitCode = 1;
35
- }
package/docs/mcp.md CHANGED
@@ -94,6 +94,8 @@ Use paths to select discovered links; submit the seed separately as `url`. Addin
94
94
 
95
95
  Page inputs accept `limit` (1–100, default 10) and one of `afterCursor`/`beforeCursor`. Copy `nextCursor` or `prevCursor` unchanged. An empty page with a cursor is not the end; stop on repeated cursors or the caller's page/call limit. Aggregate completion does not mean every page succeeded. If a page list exceeds the output limit, repeat that read from the same input cursor with a smaller `limit` (down to 1), then continue using the successful response's cursor. Advancing past a withheld page would skip unread results.
96
96
 
97
+ If one row still exceeds the cap, `error.recovery.page` retains its identity, status and available storage references when they fit the output budget. It is a summary, not the full row. Read the required references before using `error.recovery.continuation.nextCursor` or `prevCursor` to move past that row. Those cursors are omitted for multi-row errors or when no reference fits. If the summary cannot satisfy the request, stop and report the limit; do not skip the page or submit another crawl.
98
+
97
99
  ### Read a reference
98
100
 
99
101
  Pass the value of `data.markdownRef` as **`ref`**, with `format: "markdown"`:
package/docs/mcp_zh.md CHANGED
@@ -96,6 +96,8 @@ Proxy 专用变量见 [proxy_zh.md](proxy_zh.md)。未启用的模块不要求
96
96
 
97
97
  列表触发 `OUTPUT_LIMIT` 时,保持相同输入游标,减小 `limit` 至最小1后重新读取该页,再使用成功结果中的游标继续。不能跳过被隐藏的页面结果。
98
98
 
99
+ 若单条页面仍超限,`error.recovery.page` 会在输出预算允许时保留该行标识、状态及可用存储引用;它是摘要,不是完整页面结果。先读取所需引用,再用 `error.recovery.continuation.nextCursor` 或 `prevCursor` 移至该行之后或之前。多行超限或没有引用能放入预算时,不返回这些继续游标。摘要不能满足需求时,应停止并说明限制,不得跳过页面或重新提交整站任务。
100
+
99
101
  ### 引用读取
100
102
 
101
103
  把 `data.markdownRef` 的原值传给 **`ref`**,不是把 `markdownRef` 当作输入字段名:
package/docs/proxy.md CHANGED
@@ -11,25 +11,26 @@ Proxy-only requires no Crawl token. Use this client entry and replace the genera
11
11
  "mcpServers": {
12
12
  "nstdata": {
13
13
  "command": "npx",
14
- "args": ["--yes", "@nstdata-ai/mcp-server@1.0.1"],
14
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
15
15
  "env": {
16
16
  "NSTDATA_MODULES": "proxy",
17
- "NSTDATA_PROXY_URL": "http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST:PROXY_PORT",
18
- "NSTDATA_PROXY_CHECK_URL": "https://api.nstdata.io/api/v2/public/utility/client-ip"
17
+ "NSTDATA_PROXY_URL": "http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST:PROXY_PORT"
19
18
  }
20
19
  }
21
20
  }
22
21
  }
23
22
  ```
24
23
 
25
- For both modules, set `NSTDATA_MODULES=crawl,proxy` and add `NSTDATA_API_TOKEN` in this same entry. The check URL is operator-selected; the Nstdata echo endpoint above returns the IP observed for that request.
24
+ For both modules, set `NSTDATA_MODULES=crawl,proxy` and add `NSTDATA_API_TOKEN` in this same entry. Startup sends no probe; only an explicit `nstdata_proxy_check` call accesses the check target.
26
25
 
27
26
  | Variable | Purpose |
28
27
  |---|---|
29
28
  | `NSTDATA_PROXY_URL` | Required HTTP/HTTPS gateway URL, including encoded username/password when needed |
30
- | `NSTDATA_PROXY_CHECK_URL` | Required HTTP/HTTPS target for `nstdata_proxy_check` |
29
+ | `NSTDATA_PROXY_CHECK_URL` | Optional HTTP/HTTPS check target; defaults to `https://example.com/` |
31
30
  | `NSTDATA_PROXY_CA_FILE` | Optional absolute path to a PEM CA bundle for Proxy TLS only |
32
31
 
32
+ The default target checks connectivity and does not echo the exit IP. To observe that IP, set `NSTDATA_PROXY_CHECK_URL` to `https://api.nstdata.io/api/v2/public/utility/client-ip` or an IP echo target you trust. The example domain offers no monitoring availability guarantee.
33
+
33
34
  Use the full generated Nstdata or PM Router URL. Preserve channel/region/session/task username syntax. Gateway and target can each be HTTP or HTTPS; HTTPS targets use CONNECT. SOCKS proxies are not supported.
34
35
 
35
36
  CA files must be regular files, at most 1 MiB, containing parseable certificates. Trust is added to Node's default CA set; gateway/target hostname verification stays enabled. Crawl trust settings are unchanged.
package/docs/proxy_zh.md CHANGED
@@ -11,11 +11,10 @@
11
11
  "mcpServers": {
12
12
  "nstdata": {
13
13
  "command": "npx",
14
- "args": ["--yes", "@nstdata-ai/mcp-server@1.0.1"],
14
+ "args": ["--yes", "@nstdata-ai/mcp-server@1.0.2"],
15
15
  "env": {
16
16
  "NSTDATA_MODULES": "proxy",
17
- "NSTDATA_PROXY_URL": "http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST:PROXY_PORT",
18
- "NSTDATA_PROXY_CHECK_URL": "https://api.nstdata.io/api/v2/public/utility/client-ip"
17
+ "NSTDATA_PROXY_URL": "http://PROXY_USER:PROXY_PASSWORD@PROXY_HOST:PROXY_PORT"
19
18
  }
20
19
  }
21
20
  }
@@ -23,14 +22,16 @@
23
22
 
24
23
  ```
25
24
 
26
- 同时启用两模块时,在同一条目设置 `NSTDATA_MODULES=crawl,proxy` 并补充 `NSTDATA_API_TOKEN`。检测地址由操作者选择;上述 Nstdata 回显接口返回该次请求被观测到的 IP。
25
+ 同时启用两模块时,在同一条目设置 `NSTDATA_MODULES=crawl,proxy` 并补充 `NSTDATA_API_TOKEN`。启动不发检测请求;调用 `nstdata_proxy_check` 时才访问检测目标。
27
26
 
28
27
  | 变量 | 用途 |
29
28
  |---|---|
30
29
  | `NSTDATA_PROXY_URL` | 必填HTTP/HTTPS网关URL;需要时包含编码后的用户名密码 |
31
- | `NSTDATA_PROXY_CHECK_URL` | `nstdata_proxy_check` 的必填HTTP/HTTPS目标 |
30
+ | `NSTDATA_PROXY_CHECK_URL` | 可选HTTP/HTTPS检测目标,默认为 `https://example.com/` |
32
31
  | `NSTDATA_PROXY_CA_FILE` | 可选PEM CA文件绝对路径,仅用于Proxy TLS |
33
32
 
33
+ 默认目标用于连通性检测,不返回出口 IP。需要出口 IP 时,将 `NSTDATA_PROXY_CHECK_URL` 设为 `https://api.nstdata.io/api/v2/public/utility/client-ip` 或你信任的 IP 回显目标。示例域名不提供监控可用性保证。
34
+
34
35
  使用完整的平台或PM Router连接,保留通道/地域/会话/任务的用户名语法。网关和目标各自可为HTTP或HTTPS;HTTPS目标使用CONNECT。不支持SOCKS代理。
35
36
 
36
37
  CA文件必须为普通文件、最多1 MiB,并包含可解析证书。信任追加到Node默认CA,网关/目标主机名校验保持开启,不改变Crawl信任设置。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nstdata-ai/mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "MCP access to Nstdata web data capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",