@neopress/mcp 1.4.0 → 1.5.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/README.md +2 -2
- package/dist/bin.cjs +348 -23926
- package/dist/http-bin.cjs +611 -23917
- package/dist/index.d.ts +76 -64
- package/dist/index.js +256 -23858
- package/integrations/submission-guide.md +5 -4
- package/package.json +4 -3
- package/scripts/check-dist-contract.mjs +59 -31
- package/scripts/prepare-mcpb-package.mjs +29 -35
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Neopress 리모트 MCP — 등재까지 사람 가이드
|
|
2
2
|
|
|
3
|
-
작성 2026-07-28.
|
|
4
|
-
|
|
3
|
+
작성 2026-07-28. 2026-09-07 코드 기준: RS-only 전환은 이미 main에 반영돼 있다. 아래 인프라 절차는 신규 환경 구성·재검증용이며 현재 배포나 등재 완료를 의미하지 않는다.
|
|
4
|
+
|
|
5
|
+
외부 서비스의 제출 자격·화면·심사 정책은 7월 조사 메모다. 실제 제출 시 각 서비스 공식 문서에서 다시 확인한다. 제출 초안은 `claude-remote/connector-draft.md`, `chatgpt/app-draft.md`에 있고, support URL·리뷰 환경·실제 OAuth 왕복 확인은 별도 완료가 필요하다.
|
|
5
6
|
|
|
6
7
|
## 이미 끝나 있는 것 (코드·문서 — 사람 액션 불요)
|
|
7
8
|
|
|
8
|
-
- 리모트 MCP 서버(RS-only, Supabase OAuth 2.1 Server 기반), 동의 페이지, Redis 영속화, CI 배포 워크플로우, 도메인 검증 라우트(`/.well-known/openai-apps-challenge`), 계약 게이트
|
|
9
|
+
- 리모트 MCP 서버(RS-only, Supabase OAuth 2.1 Server 기반), 동의 페이지, Redis 영속화, CI 배포 워크플로우, 도메인 검증 라우트(`/.well-known/openai-apps-challenge`), `pnpm check:agent-contracts` 계약 게이트
|
|
9
10
|
- 제출 컬래터럴: 리스팅 문구, OAuth 설정 명세, 툴 안전성 요약, 리뷰 테스트 프롬프트/케이스(양성 5·음성 3), privacy(`https://app.neopress.ai/en/privacy`)·terms(`/en/terms`) URL
|
|
10
11
|
|
|
11
12
|
## 지금 바로 시작해두면 좋은 것 (리드타임 있음 — 순서 무관)
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
|
|
18
19
|
## Phase 1 — 인프라 (머지 후, 약 30–60분)
|
|
19
20
|
|
|
20
|
-
1.
|
|
21
|
+
1. 배포할 main 커밋과 대상 환경을 확인한다. RS-only 코드 전환은 완료돼 있다.
|
|
21
22
|
2. **Upstash Redis**: 인스턴스 준비(기존 재사용 가능) → `UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN` 확보.
|
|
22
23
|
3. **GitHub 설정**: 레포 Settings → Secrets에 `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN` (+ 나중에 `OPENAI_APPS_CHALLENGE_TOKEN`), Variables에 `NEOPRESS_MCP_PUBLIC_URL=https://mcp.neopress.ai`. ⚠️ 머지 직후 첫 자동 CI run은 secrets 없으면 실패가 정상 — 등록 후 재실행.
|
|
23
24
|
4. **Supabase 대시보드** (프로젝트 gxrfyjmsmiakirmtzkmv): Authentication → **OAuth Server 활성 + Dynamic Client Registration 활성 + Authorization Path = `https://app.neopress.ai/oauth/consent`**.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neopress/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Neopress MCP server for first-party AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
"tsdown": "^0.22.14",
|
|
38
38
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
39
39
|
"vitest": "^4.1.5",
|
|
40
|
-
"@neopress/
|
|
41
|
-
"@neopress/sdk": "0.4.0"
|
|
40
|
+
"@neopress/logging": "0.0.0",
|
|
41
|
+
"@neopress/sdk": "0.4.0",
|
|
42
|
+
"@neopress/shared": "0.0.0"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "NODE_OPTIONS=--max-old-space-size=4096 tsdown && node ./scripts/check-dist-contract.mjs",
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// for the rationale; the file-set invariant matters more here because
|
|
4
4
|
// prepare-mcpb-package.mjs copies dist/bin.cjs alone into the Claude Desktop bundle,
|
|
5
5
|
// so a shared chunk would produce an extension that breaks at runtime.
|
|
6
|
-
import { readFileSync, readdirSync } from
|
|
7
|
-
import { builtinModules } from
|
|
8
|
-
import path from
|
|
6
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { builtinModules } from "node:module";
|
|
8
|
+
import path from "node:path";
|
|
9
9
|
|
|
10
|
-
const packageRoot = path.resolve(import.meta.dirname,
|
|
11
|
-
const distDir = path.join(packageRoot,
|
|
12
|
-
const packageJson = JSON.parse(readFileSync(path.join(packageRoot,
|
|
10
|
+
const packageRoot = path.resolve(import.meta.dirname, "..");
|
|
11
|
+
const distDir = path.join(packageRoot, "dist");
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
13
13
|
|
|
14
|
-
const expectedFiles = [
|
|
14
|
+
const expectedFiles = ["bin.cjs", "http-bin.cjs", "index.d.ts", "index.js"];
|
|
15
15
|
const allowedBareImports = new Set(Object.keys(packageJson.dependencies ?? {}));
|
|
16
16
|
// See the cli script: tsdown inlines a leaked third-party type instead of leaving a
|
|
17
17
|
// dangling import, turning a loud failure into silent bloat. Current output is ~26KB.
|
|
@@ -24,13 +24,13 @@ const unexpected = actualFiles.filter((f) => !expectedFiles.includes(f));
|
|
|
24
24
|
const missing = expectedFiles.filter((f) => !actualFiles.includes(f));
|
|
25
25
|
if (unexpected.length > 0) {
|
|
26
26
|
errors.push(
|
|
27
|
-
`dist has unexpected files: ${unexpected.join(
|
|
27
|
+
`dist has unexpected files: ${unexpected.join(", ")}\n` +
|
|
28
28
|
` Likely code splitting — give each entry its own config object in tsdown.config.ts.\n` +
|
|
29
|
-
` dist/bin.cjs must stay self-contained: prepare-mcpb-package.mjs ships it alone
|
|
29
|
+
` dist/bin.cjs must stay self-contained: prepare-mcpb-package.mjs ships it alone.`,
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
if (missing.length > 0) {
|
|
33
|
-
errors.push(`dist is missing: ${missing.join(
|
|
33
|
+
errors.push(`dist is missing: ${missing.join(", ")}`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Every emitted bundle must have the workspace packages inlined. tsdown reports an
|
|
@@ -38,18 +38,18 @@ if (missing.length > 0) {
|
|
|
38
38
|
// which would publish a binary carrying a bare require("@neopress/sdk") that no consumer
|
|
39
39
|
// can resolve. Checking the .d.ts alone does not see this.
|
|
40
40
|
for (const file of actualFiles.filter((f) => /\.(cjs|js|mjs)$/.test(f))) {
|
|
41
|
-
const bundle = readFileSync(path.join(distDir, file),
|
|
41
|
+
const bundle = readFileSync(path.join(distDir, file), "utf8");
|
|
42
42
|
const leaked = [
|
|
43
43
|
...new Set(
|
|
44
|
-
[...bundle.matchAll(/(?:require\(|from\s*)["'](@neopress\/[^"']+)["']/g)].map((m) => m[1])
|
|
44
|
+
[...bundle.matchAll(/(?:require\(|from\s*)["'](@neopress\/[^"']+)["']/g)].map((m) => m[1]),
|
|
45
45
|
),
|
|
46
46
|
];
|
|
47
47
|
if (leaked.length > 0) {
|
|
48
48
|
errors.push(
|
|
49
|
-
`dist/${file} still imports workspace packages at runtime: ${leaked.join(
|
|
49
|
+
`dist/${file} still imports workspace packages at runtime: ${leaked.join(", ")}\n` +
|
|
50
50
|
` They must be bundled. tsdown only warns on unresolved imports, so a bare\n` +
|
|
51
51
|
` require would ship — sdk exports src directly, so this means the import failed\n` +
|
|
52
|
-
` to resolve (renamed source file or export-map drift), not a missing prebuild
|
|
52
|
+
` to resolve (renamed source file or export-map drift), not a missing prebuild.`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -57,7 +57,7 @@ for (const file of actualFiles.filter((f) => /\.(cjs|js|mjs)$/.test(f))) {
|
|
|
57
57
|
// alone; the Docker runtime stage copies dist/ alone), so a declared
|
|
58
58
|
// dependency left external is just as fatal there as a workspace leak — the
|
|
59
59
|
// allowedBareImports pass above only protects the library entry.
|
|
60
|
-
if (file.endsWith(
|
|
60
|
+
if (file.endsWith(".cjs")) {
|
|
61
61
|
// The backtick lookbehind skips require literals inside codegen templates:
|
|
62
62
|
// ajv's standalone-validator generator embeds `require("ajv/dist/...")` as
|
|
63
63
|
// template text that never executes (the modules themselves are inlined).
|
|
@@ -66,29 +66,29 @@ for (const file of actualFiles.filter((f) => /\.(cjs|js|mjs)$/.test(f))) {
|
|
|
66
66
|
[...bundle.matchAll(/(?<!`)require\(\s*["']([^"'./][^"']*)["']\s*\)/g)]
|
|
67
67
|
.map((m) => m[1])
|
|
68
68
|
.filter((specifier) => {
|
|
69
|
-
const name = specifier.replace(/^node:/,
|
|
69
|
+
const name = specifier.replace(/^node:/, "").split("/")[0];
|
|
70
70
|
return !builtinModules.includes(name);
|
|
71
|
-
})
|
|
71
|
+
}),
|
|
72
72
|
),
|
|
73
73
|
];
|
|
74
74
|
if (bare.length > 0) {
|
|
75
75
|
errors.push(
|
|
76
|
-
`dist/${file} requires packages at runtime: ${bare.join(
|
|
76
|
+
`dist/${file} requires packages at runtime: ${bare.join(", ")}\n` +
|
|
77
77
|
` This binary ships without node_modules — add the package to that entry's\n` +
|
|
78
|
-
` deps.alwaysBundle in tsdown.config.ts
|
|
78
|
+
` deps.alwaysBundle in tsdown.config.ts.`,
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const declarations = actualFiles.includes(
|
|
85
|
-
? readFileSync(path.join(distDir,
|
|
86
|
-
:
|
|
84
|
+
const declarations = actualFiles.includes("index.d.ts")
|
|
85
|
+
? readFileSync(path.join(distDir, "index.d.ts"), "utf8")
|
|
86
|
+
: "";
|
|
87
87
|
if (declarations.length > maxDeclarationBytes) {
|
|
88
88
|
errors.push(
|
|
89
89
|
`dist/index.d.ts is ${Math.round(declarations.length / 1000)}KB, over the ${Math.round(maxDeclarationBytes / 1000)}KB ceiling\n` +
|
|
90
90
|
` A third-party type tree was probably inlined. Narrow the public surface\n` +
|
|
91
|
-
` (@internal + stripInternal) rather than raising this number
|
|
91
|
+
` (@internal + stripInternal) rather than raising this number.`,
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
const specifiers = [
|
|
@@ -98,23 +98,51 @@ const specifiers = [
|
|
|
98
98
|
...[...declarations.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)].map((m) => m[1]),
|
|
99
99
|
];
|
|
100
100
|
const unresolvable = [...new Set(specifiers)].filter((specifier) => {
|
|
101
|
-
if (specifier.startsWith(
|
|
102
|
-
const packageName = specifier.startsWith(
|
|
103
|
-
? specifier.split(
|
|
104
|
-
: specifier.split(
|
|
101
|
+
if (specifier.startsWith("node:")) return false;
|
|
102
|
+
const packageName = specifier.startsWith("@")
|
|
103
|
+
? specifier.split("/").slice(0, 2).join("/")
|
|
104
|
+
: specifier.split("/")[0];
|
|
105
105
|
return !allowedBareImports.has(packageName);
|
|
106
106
|
});
|
|
107
107
|
if (unresolvable.length > 0) {
|
|
108
108
|
errors.push(
|
|
109
|
-
`dist/index.d.ts imports that consumers cannot resolve: ${unresolvable.join(
|
|
109
|
+
`dist/index.d.ts imports that consumers cannot resolve: ${unresolvable.join(", ")}\n` +
|
|
110
110
|
` Allowed: node: builtins and this package's own dependencies.\n` +
|
|
111
|
-
` Relative imports mean a type was not inlined; bare ones mean an undeclared dependency leaked
|
|
111
|
+
` Relative imports mean a type was not inlined; bare ones mean an undeclared dependency leaked.`,
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// ── 3. The SDK client stays HTTP-only ───────────────────────────────────────
|
|
116
|
+
// `NeopressClientOptions` is inlined into this .d.ts because the public surface
|
|
117
|
+
// returns a `NeopressClient`. That makes the emitted declaration the enforceable
|
|
118
|
+
// record of the option bag — assert the interface is still here (so the check
|
|
119
|
+
// cannot go vacuous behind an export change) and that the direct-DB pair is not.
|
|
120
|
+
const clientOptions = declarations.match(/interface NeopressClientOptions\s*\{([\s\S]*?)\n\}/);
|
|
121
|
+
if (!clientOptions) {
|
|
122
|
+
errors.push(
|
|
123
|
+
`dist/index.d.ts no longer declares NeopressClientOptions\n` +
|
|
124
|
+
` The W7 direct-DB guard reads that interface. If the surface legitimately\n` +
|
|
125
|
+
` stopped exposing NeopressClient, re-point the guard instead of dropping it.`,
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
const reopened = ["supabaseUrl", "supabaseAnonKey"].filter((member) =>
|
|
129
|
+
new RegExp(`^\\s*${member}\\??:`, "m").test(clientOptions[1]),
|
|
130
|
+
);
|
|
131
|
+
if (reopened.length > 0) {
|
|
132
|
+
errors.push(
|
|
133
|
+
`NeopressClientOptions re-declares direct-DB config: ${reopened.join(", ")}\n` +
|
|
134
|
+
` The SDK must reach the API over HTTP only — assets/collections/entries/\n` +
|
|
135
|
+
` entry_references/forms/pages/redirect_rules revoked their authenticated\n` +
|
|
136
|
+
` write grants on that basis (docs/master/2026-09-07-billing-and-access.md).`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
if (errors.length > 0) {
|
|
116
|
-
console.error(`[check-dist-contract] ${packageJson.name} failed:\n\n${errors.join(
|
|
142
|
+
console.error(`[check-dist-contract] ${packageJson.name} failed:\n\n${errors.join("\n\n")}\n`);
|
|
117
143
|
process.exit(1);
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
console.log(
|
|
146
|
+
console.log(
|
|
147
|
+
`[check-dist-contract] ${packageJson.name} ok — ${actualFiles.length} files, no unresolvable type imports`,
|
|
148
|
+
);
|
|
@@ -1,54 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
mkdirSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'node:fs';
|
|
9
|
-
import { dirname, resolve } from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
11
5
|
|
|
12
|
-
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)),
|
|
13
|
-
const distBin = resolve(packageRoot,
|
|
14
|
-
const manifest = resolve(packageRoot,
|
|
15
|
-
const outDir = resolve(packageRoot,
|
|
16
|
-
const outServerDir = resolve(outDir,
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const distBin = resolve(packageRoot, "dist/bin.cjs");
|
|
8
|
+
const manifest = resolve(packageRoot, "integrations/claude-desktop/manifest.json");
|
|
9
|
+
const outDir = resolve(packageRoot, "dist/mcpb/neopress");
|
|
10
|
+
const outServerDir = resolve(outDir, "server");
|
|
17
11
|
|
|
18
12
|
if (!existsSync(distBin)) {
|
|
19
|
-
console.error(
|
|
13
|
+
console.error("Missing dist/bin.cjs. Run `pnpm --filter @neopress/mcp build` first.");
|
|
20
14
|
process.exit(1);
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
if (!existsSync(manifest)) {
|
|
24
|
-
console.error(
|
|
18
|
+
console.error("Missing Claude Desktop MCPB manifest.");
|
|
25
19
|
process.exit(1);
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
rmSync(outDir, { recursive: true, force: true });
|
|
29
23
|
mkdirSync(outServerDir, { recursive: true });
|
|
30
24
|
|
|
31
|
-
copyFileSync(manifest, resolve(outDir,
|
|
32
|
-
copyFileSync(distBin, resolve(outServerDir,
|
|
25
|
+
copyFileSync(manifest, resolve(outDir, "manifest.json"));
|
|
26
|
+
copyFileSync(distBin, resolve(outServerDir, "bin.cjs"));
|
|
33
27
|
writeFileSync(
|
|
34
|
-
resolve(outDir,
|
|
28
|
+
resolve(outDir, "README.md"),
|
|
35
29
|
[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
].join(
|
|
51
|
-
|
|
30
|
+
"# Neopress MCPB Draft",
|
|
31
|
+
"",
|
|
32
|
+
"This bundle stages the local Neopress stdio MCP server for Claude Desktop.",
|
|
33
|
+
"",
|
|
34
|
+
"Before installing, run `neopress login` so the MCP server can reuse and refresh",
|
|
35
|
+
"the first-party OAuth session. Alternatively, configure a short-lived OAuth",
|
|
36
|
+
"access token in the extension settings for testing only.",
|
|
37
|
+
"",
|
|
38
|
+
"To pack this directory:",
|
|
39
|
+
"",
|
|
40
|
+
"```bash",
|
|
41
|
+
"pnpm dlx @anthropic-ai/mcpb pack packages/mcp/dist/mcpb/neopress",
|
|
42
|
+
"```",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n"),
|
|
45
|
+
"utf-8",
|
|
52
46
|
);
|
|
53
47
|
|
|
54
48
|
console.log(`Staged MCPB directory: ${outDir}`);
|