mcp-scraper 0.1.9 → 0.2.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/dist/bin/api-server.cjs +845 -40
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +314 -0
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -0
- package/dist/bin/browser-agent-stdio-server.d.cts +1 -0
- package/dist/bin/browser-agent-stdio-server.d.ts +1 -0
- package/dist/bin/browser-agent-stdio-server.js +313 -0
- package/dist/bin/browser-agent-stdio-server.js.map +1 -0
- package/dist/bin/mcp-stdio-server.cjs +1 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -1
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/chunk-2BS7BUEE.js +7 -0
- package/dist/chunk-2BS7BUEE.js.map +1 -0
- package/dist/{chunk-JNC32DMS.js → chunk-BMVQB3WN.js} +4 -4
- package/dist/chunk-BMVQB3WN.js.map +1 -0
- package/dist/{chunk-ZK456YXN.js → chunk-GXBT5CDU.js} +20 -3
- package/dist/chunk-GXBT5CDU.js.map +1 -0
- package/dist/{server-MTXAJG5J.js → server-ASCMKUQ5.js} +790 -28
- package/dist/server-ASCMKUQ5.js.map +1 -0
- package/dist/{worker-AUCXFHEL.js → worker-KJ4A7WIR.js} +2 -2
- package/docs/specs/api-forge-spec.md +234 -0
- package/docs/specs/deferred-work-spec.md +74 -0
- package/docs/specs/oauth-mcp-spec.md +213 -0
- package/package.json +3 -2
- package/dist/chunk-JNC32DMS.js.map +0 -1
- package/dist/chunk-ZK456YXN.js.map +0 -1
- package/dist/server-MTXAJG5J.js.map +0 -1
- /package/dist/{worker-AUCXFHEL.js.map → worker-KJ4A7WIR.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createHarvestAttemptRecorder,
|
|
5
5
|
harvestProblemResponse,
|
|
6
6
|
serializeHarvestProblem
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GXBT5CDU.js";
|
|
8
8
|
import {
|
|
9
9
|
browserServiceApiKey,
|
|
10
10
|
harvest
|
|
@@ -122,4 +122,4 @@ export {
|
|
|
122
122
|
startWorker,
|
|
123
123
|
tickOnce
|
|
124
124
|
};
|
|
125
|
-
//# sourceMappingURL=worker-
|
|
125
|
+
//# sourceMappingURL=worker-KJ4A7WIR.js.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# API Forge — Implementation Spec
|
|
2
|
+
|
|
3
|
+
Research-grounded API design intelligence inside MCP Scraper. A database-design tool asks about entities and relationships, then emits DDL. API Forge asks about resources, operations, and consumers — then emits OpenAPI, Zod schemas, SQL DDL, and **MCP tool definitions that comply with `docs/mcp-tool-quality-spec.md`** — with every design decision groundable in live web evidence harvested by the existing scraper tools.
|
|
4
|
+
|
|
5
|
+
## The three-party design
|
|
6
|
+
|
|
7
|
+
This is the core architecture decision and the creative center of the feature:
|
|
8
|
+
|
|
9
|
+
| Party | Owns | Never does |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **MCP Scraper server** | Methodology (phase machine, question cards), session state, research execution, artifact generation, critique rules | Conversation with the human |
|
|
12
|
+
| **Calling LLM** (Claude, etc.) | Interviewing the human, translating answers into structured form, narrating findings | Design methodology, state |
|
|
13
|
+
| **Scraper tools** (existing) | Evidence: PAA questions developers ask, competitor API doc conventions, domain entity discovery | — |
|
|
14
|
+
|
|
15
|
+
The server returns *question cards* and *synthesis instructions*; the calling model conducts the interview and reports structured answers back. This works on every MCP host (no dependency on elicitation support) and keeps the methodology versioned server-side.
|
|
16
|
+
|
|
17
|
+
## Tool surface (5 new MCP tools)
|
|
18
|
+
|
|
19
|
+
All registered in `src/mcp/paa-mcp-server.ts`, schemas in `src/mcp/mcp-tool-schemas.ts`, formatters in `src/mcp/mcp-response-formatter.ts`. All have `outputSchema` + `structuredContent` (these are chaining tools by definition). Annotations: `api_critique` is `readOnlyHint: true`; the four session tools are `readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true` (they mutate server-side session state).
|
|
20
|
+
|
|
21
|
+
### 1. `api_design_start`
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
inputSchema: {
|
|
25
|
+
name: z.string().min(1).describe('Working name of the API, e.g. "Acme Bookings API"'),
|
|
26
|
+
purpose: z.string().min(1).describe('One-sentence purpose in the user\'s words'),
|
|
27
|
+
domain: z.string().min(1).describe('Business domain, e.g. "salon appointment booking" — used to seed research'),
|
|
28
|
+
research: z.boolean().default(true).describe('Run an automatic domain research pass (PAA + SERP) before the first interview card. Costs credits per the cost table.'),
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Behavior: creates a session, optionally runs Phase-0 research (internal calls to `harvest()` with `maxQuestions: 15` and query `"${domain} api"` — direct function call, not HTTP; billed via existing `paa` ledger op), and returns the first question card.
|
|
33
|
+
|
|
34
|
+
Description (model-facing, per quality spec): "Start an API design session. Returns a sessionId, optional domain research findings, and the first interview card — a batch of design questions. Interview the user conversationally using the card, then submit their answers with api_design_answer. Do not invent answers the user did not give."
|
|
35
|
+
|
|
36
|
+
`outputSchema`: `{ sessionId, phase, card: CardOutput, research: { paaQuestions: string[], topCompetitorUrls: string[] } | null }`
|
|
37
|
+
|
|
38
|
+
### 2. `api_design_answer`
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
inputSchema: {
|
|
42
|
+
sessionId: z.string().min(1),
|
|
43
|
+
answers: z.array(z.object({
|
|
44
|
+
questionId: z.string().min(1),
|
|
45
|
+
value: z.string().min(1).describe('The user\'s answer, verbatim or faithfully summarized'),
|
|
46
|
+
skipped: z.boolean().default(false),
|
|
47
|
+
})).min(1),
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Behavior: validates answers against the current card, folds them into the `DesignModel` via per-question reducers, advances the phase machine when the card is satisfied, returns the next card (or `phase: 'complete'`). Free (no debit) — answers are state writes only.
|
|
52
|
+
|
|
53
|
+
`outputSchema`: `{ sessionId, phase, accepted: number, designSummary: DesignSummaryOutput, card: CardOutput | null, complete: boolean }`
|
|
54
|
+
|
|
55
|
+
### 3. `api_design_research`
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
inputSchema: {
|
|
59
|
+
sessionId: z.string().min(1),
|
|
60
|
+
competitorUrls: z.array(z.string().url()).max(5).optional().describe('API documentation URLs to mine for conventions (pagination idiom, error shape, naming, auth)'),
|
|
61
|
+
topic: z.string().optional().describe('Free-form research topic, e.g. "webhook retry best practices" — runs a PAA harvest'),
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Behavior: for each competitor URL, runs the existing `extract_url` pipeline internally, then a **deterministic convention extractor** (no server-side LLM): regex/heading analysis of the extracted Markdown detecting pagination style (`cursor|page|offset` token frequency), error envelope (`"error"`/`"errors"`/problem+json mentions), naming case (snake vs camel ratio in code blocks), auth scheme mentions. For `topic`, runs a PAA harvest. Results are stored as `evidence[]` entries on the session and returned with a `synthesisInstruction` string telling the calling model how to present findings and which open design questions the evidence bears on. Billed: `page_scrape` per URL, `paa` per question — existing ledger ops, no new rates needed.
|
|
66
|
+
|
|
67
|
+
### 4. `api_design_blueprint`
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
inputSchema: {
|
|
71
|
+
sessionId: z.string().min(1),
|
|
72
|
+
formats: z.array(z.enum(['openapi', 'zod', 'sql', 'mcp_tools', 'readme'])).min(1)
|
|
73
|
+
.default(['openapi', 'zod', 'sql', 'mcp_tools', 'readme']),
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Behavior: runs the generators (below) against the materialized `DesignModel`. Allowed mid-interview (partial blueprint from whatever is designed so far — this enables the "show me where we are" loop). Returns artifacts inline as fenced code blocks in `content` plus `structuredContent.artifacts: [{ format, filename, source }]`. On stdio, each artifact is also saved through the existing `saveFullReport` path, which makes blueprints appear in the `report://` resources list for free. New rate: `api_blueprint` 1 000 mc (1 credit) per generation.
|
|
78
|
+
|
|
79
|
+
### 5. `api_critique`
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
inputSchema: {
|
|
83
|
+
openapiUrl: z.string().url().optional().describe('URL of an existing OpenAPI/Swagger document, or API docs page'),
|
|
84
|
+
openapiSource: z.string().optional().describe('Pasted OpenAPI YAML/JSON when no URL is available'),
|
|
85
|
+
audience: z.enum(['human', 'service', 'ai_agent']).default('ai_agent').describe('Score the API for this consumer. ai_agent applies MCP-readiness rules.'),
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Behavior: fetch (via internal `extract_url` when URL) → parse OpenAPI → run the critique rule table → return scored findings `[{ ruleId, severity: 'P0'|'P1'|'P2', location, finding, fix }]`. The `ai_agent` audience adds the MCP-readiness rules derived from `docs/mcp-tool-quality-spec.md` (description quality, enum documentation, error actionability, schema-encoded defaults/caps). This is the standalone marketing wedge: "run our quality spec against *your* API." New rate: `api_critique` 2 000 mc.
|
|
90
|
+
|
|
91
|
+
## Reverse Forge (the second creative wedge — Phase 6, optional)
|
|
92
|
+
|
|
93
|
+
`api_design_start` accepts an optional `importUrl`: point it at an existing API's docs site; the server runs `map_site_urls` + `extract_url` over the docs internally, reconstructs a partial `DesignModel` from the conventions extractor, and starts the interview at the *gaps* instead of from zero. The session then supports "extend this API with X" with consistency checks against the inferred conventions (e.g. "your existing endpoints use cursor pagination and snake_case; the new webhooks resource will follow"). Implemented as `meta.importedFrom` on the session plus a `seedDesignFromDocs(extracts: ExtractResult[]): Partial<DesignModel>` function in the conventions extractor module.
|
|
94
|
+
|
|
95
|
+
## The interview engine
|
|
96
|
+
|
|
97
|
+
### `src/forge/design-model.ts` — exact types
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export interface DesignField { name: string; type: 'string'|'number'|'boolean'|'timestamp'|'json'|'ref'; refEntity?: string; required: boolean; description: string }
|
|
101
|
+
export interface DesignEntity { name: string; plural: string; identity: 'uuid'|'slug'|'int'; fields: DesignField[] }
|
|
102
|
+
export interface DesignRelationship { from: string; to: string; kind: 'one_to_one'|'one_to_many'|'many_to_many'; ownership: 'embedded'|'referenced' }
|
|
103
|
+
export interface DesignOperation { entity: string; verb: 'list'|'get'|'create'|'update'|'delete'|'action'; actionName?: string; idempotent: boolean; paginated: boolean; description: string }
|
|
104
|
+
export interface DesignContracts { errorShape: 'problem_json'|'envelope'|'custom'; pagination: 'cursor'|'offset'|'none'; naming: 'snake'|'camel'; envelope: boolean }
|
|
105
|
+
export interface DesignAuth { scheme: 'api_key'|'oauth2'|'jwt'|'none'; scopes: string[] }
|
|
106
|
+
export interface DesignEvidence { source: 'paa'|'serp'|'competitor_doc'|'user'; url: string | null; finding: string; appliedTo: string }
|
|
107
|
+
export interface DesignModel {
|
|
108
|
+
meta: { name: string; purpose: string; domain: string; consumers: Array<'human'|'service'|'ai_agent'>; importedFrom?: string }
|
|
109
|
+
entities: DesignEntity[]
|
|
110
|
+
relationships: DesignRelationship[]
|
|
111
|
+
operations: DesignOperation[]
|
|
112
|
+
contracts: DesignContracts
|
|
113
|
+
auth: DesignAuth
|
|
114
|
+
evidence: DesignEvidence[]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `src/forge/question-cards.ts` — phase machine as data
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export type ForgePhase = 'consumers'|'entities'|'relationships'|'operations'|'contracts'|'auth'|'review'|'complete'
|
|
122
|
+
export interface QuestionCard {
|
|
123
|
+
phase: ForgePhase
|
|
124
|
+
intro: string // one paragraph the model relays before asking
|
|
125
|
+
questions: Array<{
|
|
126
|
+
id: string // e.g. 'entities.list'
|
|
127
|
+
ask: string // the question, written to be read aloud
|
|
128
|
+
why: string // rationale the model can offer if asked
|
|
129
|
+
expects: 'free_text'|'entity_list'|'choice'|'yes_no'
|
|
130
|
+
choices?: string[]
|
|
131
|
+
skippable: boolean
|
|
132
|
+
appliesIf?: (model: DesignModel) => boolean // skip logic, e.g. m2m junction question only if any many_to_many
|
|
133
|
+
}>
|
|
134
|
+
}
|
|
135
|
+
export const CARDS: Record<ForgePhase, QuestionCard>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Phase order and headline questions (full card text lives in the file):
|
|
139
|
+
|
|
140
|
+
1. **consumers** — "Who calls this API: humans via a UI, backend services, or AI agents?" (multi-choice; `ai_agent` switches on MCP generation + agent-readability rules). "What must never be exposed?" (seeds redaction rules — mirrors this repo's vendor-concealment pattern).
|
|
141
|
+
2. **entities** — "Name the things your API manages, singular nouns." Then per entity: identity strategy, 3–7 core fields. Reducer: `entities.list` answer is parsed as comma/newline-separated nouns; per-entity follow-up cards are generated dynamically (`entities.fields.{name}`).
|
|
142
|
+
3. **relationships** — for each entity pair that co-occurred in answers: "Does an X own many Y?" Cardinality + embedded-vs-referenced.
|
|
143
|
+
4. **operations** — per entity: which of list/get/create/update/delete, plus domain actions ("cancel a booking" → `POST /bookings/{id}/cancel`, `idempotent: false`).
|
|
144
|
+
5. **contracts** — pagination, error shape, naming. **This is where research lands**: if the session has competitor evidence, the card's `intro` includes "Competitors X and Y both use cursor pagination" and the question defaults accordingly.
|
|
145
|
+
6. **auth** — scheme + scopes; if consumers includes `ai_agent`, ask about per-call cost/metering (seeds a credits-style design like this repo's).
|
|
146
|
+
7. **review** — the server returns a `DesignSummary`; the model walks the user through it; any "change X" answers route back to the owning phase.
|
|
147
|
+
|
|
148
|
+
### `src/forge/interview-engine.ts`
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export function nextCard(model: DesignModel, phase: ForgePhase): { card: QuestionCard | null; phase: ForgePhase }
|
|
152
|
+
export function applyAnswers(model: DesignModel, phase: ForgePhase, answers: Answer[]): { model: DesignModel; errors: string[] }
|
|
153
|
+
export function designSummary(model: DesignModel): DesignSummaryOutput
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pure functions — no I/O — so the whole engine is unit-testable with golden sessions.
|
|
157
|
+
|
|
158
|
+
## Generators — `src/forge/generators/`
|
|
159
|
+
|
|
160
|
+
Each is `(model: DesignModel) => { filename: string; source: string }`, pure, individually golden-tested.
|
|
161
|
+
|
|
162
|
+
- `openapi.ts` — OpenAPI 3.1 YAML. Paths from operations (`GET /bookings`, `POST /bookings/{id}/cancel`), components.schemas from entities, error responses per `contracts.errorShape`, pagination params per `contracts.pagination`, securitySchemes per auth.
|
|
163
|
+
- `zod.ts` — one Zod schema per entity + per-operation input schemas, naming per `contracts.naming`.
|
|
164
|
+
- `sql.ts` — SQLite-flavored DDL: one table per entity, junction tables for `many_to_many`, FK columns for `referenced` one-to-many, `TEXT` ISO timestamps (house style of this repo's `db.ts`).
|
|
165
|
+
- `mcp-tools.ts` — the dogfood crown: emits a `registerTool` block per operation following `docs/mcp-tool-quality-spec.md` — model-instructing description ("Use this when…"), schema-encoded defaults/caps, annotations (`readOnlyHint: true` for list/get), `outputSchema` for chaining ops, error-shape guidance. Generated against the same `liveWebToolAnnotations` helper pattern used in `paa-mcp-server.ts`.
|
|
166
|
+
- `readme.ts` — install + auth + per-endpoint docs with curl examples.
|
|
167
|
+
|
|
168
|
+
## Persistence — `src/api/db.ts` additions
|
|
169
|
+
|
|
170
|
+
```sql
|
|
171
|
+
CREATE TABLE IF NOT EXISTS api_forge_sessions (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
user_id INTEGER NOT NULL,
|
|
174
|
+
name TEXT NOT NULL,
|
|
175
|
+
phase TEXT NOT NULL DEFAULT 'consumers',
|
|
176
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
177
|
+
design_json TEXT NOT NULL DEFAULT '{}',
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
updated_at TEXT NOT NULL
|
|
180
|
+
);
|
|
181
|
+
CREATE TABLE IF NOT EXISTS api_forge_events (
|
|
182
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
183
|
+
session_id TEXT NOT NULL,
|
|
184
|
+
kind TEXT NOT NULL, -- 'answers' | 'research' | 'blueprint'
|
|
185
|
+
payload_json TEXT NOT NULL,
|
|
186
|
+
created_at TEXT NOT NULL
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`design_json` is the materialized DesignModel; events are the audit trail (enables replay/undo later). DB helpers: `createForgeSession`, `getForgeSession`, `updateForgeSession`, `appendForgeEvent` — same row-mapping style as `getJob`.
|
|
191
|
+
|
|
192
|
+
## Routes — `src/api/api-forge-routes.ts`
|
|
193
|
+
|
|
194
|
+
`forgeApp = new Hono<ApiKeyEnv>()` mounted at `/api-forge` in `server.ts` and added to `vercel.json` rewrites (`{ "source": "/api-forge/:path*", "destination": "/api/index" }`). Five POST routes mirroring the tools; `createApiKeyAuth`; debit-before-work + refund-on-failure exactly like `maps-routes.ts`; errors in the structured `{error, error_code, retryable}` shape. Session ownership check on every route (`session.user_id === user.id` else 404).
|
|
195
|
+
|
|
196
|
+
## Rates — `src/api/rates.ts` additions
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
api_forge_start: 2_000, // 2 credits, includes Phase-0 research overhead
|
|
200
|
+
api_blueprint: 1_000,
|
|
201
|
+
api_critique: 2_000,
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Plus `CREDIT_COST_CATALOG` entries with aliases (`'api design'`, `'blueprint'`, `'critique'`), `LedgerOperation.API_FORGE_START / API_BLUEPRINT / API_CRITIQUE` + paired `_REFUND` ops (the `ledger-refund-keys` test enforces pairing).
|
|
205
|
+
|
|
206
|
+
## MCP wiring
|
|
207
|
+
|
|
208
|
+
- Executor: 5 methods on `IMcpToolExecutor`… **no** — these are a separate concern; add `IApiForgeToolExecutor` interface in `IMcpToolExecutor.ts` (same pattern as `ISerpIntelligenceToolExecutor`), implemented by `HttpMcpToolExecutor` (`this.call('/api-forge/start', input)` etc.).
|
|
209
|
+
- Registration: `registerApiForgeTools(server, executor)` in `paa-mcp-server.ts`, called from both transports (these tools work identically hosted and stdio — session state is server-side).
|
|
210
|
+
- Formatters: each returns a Markdown report (`oneBlock`) + `structuredContent`. The blueprint formatter renders each artifact as a fenced block and saves per-artifact files on stdio.
|
|
211
|
+
|
|
212
|
+
## Tests
|
|
213
|
+
|
|
214
|
+
- `tests/unit/forge-interview-engine.test.ts` — phase transitions, skip logic, reducer correctness, a full golden session (fixture answers → expected DesignModel).
|
|
215
|
+
- `tests/unit/forge-generators.test.ts` — golden outputs per generator from a fixture DesignModel; the `mcp-tools` generator output is itself grep-asserted for quality-spec markers (`readOnlyHint`, `.default(`, `.max(`).
|
|
216
|
+
- `tests/unit/forge-conventions-extractor.test.ts` — pagination/error/naming detection from fixture doc Markdown.
|
|
217
|
+
- `tests/unit/mcp-server-registration.test.ts` — tool count 13 → 18 (stdio) / 20 (hosted); annotations table updated.
|
|
218
|
+
- `tests/live/mcp/api-forge.live.test.ts` — start → answer × 2 → blueprint over stdio against the test server; asserts artifacts parse (YAML loads, DDL contains `CREATE TABLE`).
|
|
219
|
+
|
|
220
|
+
## Build phases
|
|
221
|
+
|
|
222
|
+
1. **Engine** — design-model.ts, question-cards.ts, interview-engine.ts + unit tests. No I/O. (Largest thinking, smallest risk.)
|
|
223
|
+
2. **Persistence + routes** — db.ts tables, api-forge-routes.ts (start/answer only), rates, vercel.json rewrite.
|
|
224
|
+
3. **Generators** — all five + golden tests; blueprint route + tool.
|
|
225
|
+
4. **Research** — conventions extractor, internal harvest/extract calls, evidence folding, research tool.
|
|
226
|
+
5. **Critique** — rule table + tool (independent of sessions; can ship before 4 if desired).
|
|
227
|
+
6. **Reverse Forge** — importUrl seeding (optional, after 1–5 prove out).
|
|
228
|
+
7. **Release** — quality-spec Definition-of-Done sweep: README, `public/skills/mcp-scraper/skill.md` tool list, registration tests, live tests, version bump, vercel deploy then npm publish.
|
|
229
|
+
|
|
230
|
+
## Open decisions (resolve before Phase 2)
|
|
231
|
+
|
|
232
|
+
1. Session TTL/limits: cap active sessions per user (proposal: 10) and expire after 30 days (cron at `/cron/tick` already exists).
|
|
233
|
+
2. Should `api_design_answer` bill micro-credits to deter abuse, or stay free? (Proposal: free; starts are the gate.)
|
|
234
|
+
3. Server-side LLM synthesis: deliberately excluded — the calling model synthesizes, the server stays deterministic. Revisit only if convention extraction proves too weak on real competitor docs.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Deferred Work Spec
|
|
2
|
+
|
|
3
|
+
Items deliberately deferred from the June 2026 release train (0.1.7 → 0.1.9), specified to execution detail. Each section is independently shippable. Ordered by recommended priority.
|
|
4
|
+
|
|
5
|
+
## 1. Ops: finish the vendor env var rename (no release needed)
|
|
6
|
+
|
|
7
|
+
Code already reads `BROWSER_SERVICE_API_KEY` / `BROWSER_SERVICE_PROXY_ID` first and falls back to `KERNEL_API_KEY` / `KERNEL_PROXY_ID` (`src/lib/browser-service-env.ts`). Remaining steps are pure ops:
|
|
8
|
+
|
|
9
|
+
1. Local: add to `.env`: `BROWSER_SERVICE_API_KEY=<same value as KERNEL_API_KEY>` and `BROWSER_SERVICE_PROXY_ID=<same as KERNEL_PROXY_ID>`.
|
|
10
|
+
2. Vercel: `vercel env add BROWSER_SERVICE_API_KEY production` (paste value), same for `BROWSER_SERVICE_PROXY_ID`; redeploy (`vercel --prod --yes`).
|
|
11
|
+
3. Verify: `curl -s https://mcpscraper.dev/me -H "x-api-key: $KEY"` still 200 and a `harvest_paa` smoke call succeeds.
|
|
12
|
+
4. Only after a full green release cycle: remove `KERNEL_API_KEY`/`KERNEL_PROXY_ID` from Vercel and `.env`, then delete the fallback reads in `src/lib/browser-service-env.ts:2` and `:7` and the `?? process.env.KERNEL_API_KEY` in `src/api/env.ts:35`.
|
|
13
|
+
5. Update live test requirements at the same time as step 4: `tests/live/mcp/mcp-stdio-protocol.live.test.ts:23` and `tests/live/mcp/mcp-http-protocol.live.test.ts:9` — change `requireLiveEnv([... 'KERNEL_API_KEY'])` to `'BROWSER_SERVICE_API_KEY'`.
|
|
14
|
+
|
|
15
|
+
## 2. Ops: repair the git remote
|
|
16
|
+
|
|
17
|
+
`git push` fails with "Repository not found"; release commits 0.1.6–0.1.9 exist only locally (single point of failure on this machine).
|
|
18
|
+
|
|
19
|
+
1. Decide: fix the existing `origin` URL (`git remote -v`, then `git remote set-url origin <correct-url>`) or create a fresh private repo (`gh repo create vilovieta/mcp-scraper --private --source=. --push`).
|
|
20
|
+
2. Push `main` and verify all four release commits land.
|
|
21
|
+
3. Update the `release-workflow` memory note once pushes work.
|
|
22
|
+
|
|
23
|
+
## 3. Hosted deep-harvest progress (deferred from task #7)
|
|
24
|
+
|
|
25
|
+
**Problem:** `harvest_paa` calls budgeted up to 280 s return nothing until completion. The hosted transport runs stateless JSON mode (`enableJsonResponse: true`, `mcp-routes.ts`), which cannot stream `notifications/progress`.
|
|
26
|
+
|
|
27
|
+
**Design — poll-based progress via the existing async REST API (works on BOTH transports):**
|
|
28
|
+
|
|
29
|
+
1. `src/mcp/http-mcp-tool-executor.ts` — new private method `callWithPolling(input, onProgress)`:
|
|
30
|
+
- `POST /harvest` (existing async endpoint, returns `202 { job_id }`).
|
|
31
|
+
- Poll `GET /jobs/{job_id}` every 5 s up to the existing tiered budget from `harvestTimeoutBudget`.
|
|
32
|
+
- Each poll, derive progress from the job's `attempts` array (already returned): `onProgress({ progress: attempts.at(-1)?.question_count ?? 0, total: input.maxQuestions })`.
|
|
33
|
+
- On `status: 'done'` return the result in the existing `CallToolResult` shape; on `failed`/`cancelled` return the structured error (shape already produced by `harvestProblemResponse`).
|
|
34
|
+
2. `src/mcp/paa-mcp-server.ts` — the `harvest_paa` handler gains the SDK's progress plumbing: the third callback argument `extra` exposes `sendNotification` and `_meta.progressToken`; when a token is present, forward `onProgress` events as `notifications/progress { progressToken, progress, total }`.
|
|
35
|
+
3. stdio transport: works as-is (long-lived connection).
|
|
36
|
+
4. Hosted transport: flip `mcp-routes.ts` to streaming mode — `enableJsonResponse: false` — so responses go out as SSE and interleaved progress notifications are legal. Keep `sessionIdGenerator: undefined` (still stateless per-request). Verify Vercel function streaming passes SSE through `api/index.ts`'s `streamResponse` (it already streams body chunks).
|
|
37
|
+
5. Gate: only use polling mode when `maxQuestions > 50`; smaller calls keep the current single-shot `/harvest/sync` path.
|
|
38
|
+
6. Tests: unit — fake fetch sequencing 202 → running → done, assert progress callbacks; live — stdio harvest with `maxQuestions: 60` asserting ≥1 progress notification received by the harness (add `onNotification` capture to `tests/live/harness/mcp-protocol-client.ts`).
|
|
39
|
+
|
|
40
|
+
**Estimated blast radius:** executor, server builder, mcp-routes, harness, 2 test files. No REST API changes.
|
|
41
|
+
|
|
42
|
+
## 4. Full OAuth 2.1 on hosted /mcp (deferred from task #9)
|
|
43
|
+
|
|
44
|
+
**Superseded by `docs/specs/oauth-mcp-spec.md`** — decision locked (WorkOS AuthKit, Clerk as documented alternate, self-hosted out of scope), full implementation spec with files, functions, env vars, tests, and rollout phases. The decision matrix below is retained for historical context.
|
|
45
|
+
|
|
46
|
+
Shipped already: `WWW-Authenticate: Bearer` on 401 (`mcp-routes.ts:mcpAuthError`). Remaining work requires a **product decision** before any code: which authorization server?
|
|
47
|
+
|
|
48
|
+
| Option | Effort | Notes |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| WorkOS / Auth0 in front | Low | Fastest path to claude.ai connector compatibility; monthly cost |
|
|
51
|
+
| Self-rolled AS on Turso (authorization codes + PKCE tables) | High | No vendor; significant security surface — not recommended |
|
|
52
|
+
| Stay API-key only | Zero | Forecloses web-connector onboarding; revisit when demand exists |
|
|
53
|
+
|
|
54
|
+
When a decision lands, the implementation is:
|
|
55
|
+
|
|
56
|
+
1. `GET /.well-known/oauth-protected-resource` (RFC 9728) served from `server.ts`: `{ resource: "https://mcpscraper.dev/mcp", authorization_servers: ["<AS issuer>"], bearer_methods_supported: ["header"] }`. Add a `vercel.json` rewrite for the path.
|
|
57
|
+
2. `src/mcp/mcp-routes.ts` `requireMcpCallerKey`: accept a Bearer JWT in addition to API keys — validate signature against the AS JWKS (cache keys 1 h), require `aud` containing `https://mcpscraper.dev/mcp` (RFC 8707 resource binding), map the token's subject to a user row via a new `users.oauth_subject` column (migration in `db.ts`).
|
|
58
|
+
3. Keep `x-api-key` working forever (NPX clients, REST customers).
|
|
59
|
+
4. Tests: unit for token validation paths (expired/wrong-aud/garbage); live test gated on AS test-client credentials in env.
|
|
60
|
+
|
|
61
|
+
## 5. outputSchema for the capture tools (deliberately excluded)
|
|
62
|
+
|
|
63
|
+
`capture_serp_snapshot` / `capture_serp_page_snapshots` return raw passthrough of the SERP Intelligence product payload, which is still evolving with Phoenix. Adding `outputSchema` now would either freeze that payload or break on every product iteration. Revisit when the Phoenix capture contract stabilizes; at that point: schema in `mcp-tool-schemas.ts`, a formatter (currently none — executor result passes straight through), and registration updates in `mcp-routes.ts:registerSerpIntelligenceCaptureTools`.
|
|
64
|
+
|
|
65
|
+
## 6. Maintenance follow-ups (small, batch into any release)
|
|
66
|
+
|
|
67
|
+
1. `public/skill.md` / `public/codex-skill.md` (REST-mode skill files): regenerate the `maxQuestions` guidance to say cap 200 (currently 1–150 in the parsing table) — `public/skill.md` PAA Harvest Mode STEP 1 table row `maxQuestions`.
|
|
68
|
+
2. `src/cli.ts:21` still names the flag `--kernel-api-key` (help text already neutral). Renaming the flag breaks invocations; add an alias `--browser-api-key` and hide the old one when commander supports it.
|
|
69
|
+
3. `tests/live/harness/mcp-protocol-client.ts` `close()` uses `setTimeout(resolve, 3_000)` without unref — harmless in vitest, but unref it if the harness ever runs outside vitest.
|
|
70
|
+
4. Internal naming (`BrowserDriver` kernel fields, DB columns `kernel_session_id`, `src/api/kernel-fetch.ts`, `src/kernel-proxy-resolver.ts`) intentionally unchanged — outbound sanitizers cover the boundary. Do NOT rename DB columns without a migration plan; the sanitizers make it unnecessary.
|
|
71
|
+
|
|
72
|
+
## 7. API Forge
|
|
73
|
+
|
|
74
|
+
Specified separately in `docs/specs/api-forge-spec.md`. Recommended to schedule after §1 and §2 (ops hygiene first), before §3/§4 (it produces revenue surface; progress/OAuth polish can follow demand).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# OAuth 2.1 for Hosted /mcp — Implementation Spec
|
|
2
|
+
|
|
3
|
+
Lets MCP clients that support OAuth (claude.ai web connectors, future hosts) connect to `https://mcpscraper.dev/mcp` with a "Connect → log in → approve" flow instead of a pasted API key. API keys keep working forever; OAuth is additive.
|
|
4
|
+
|
|
5
|
+
## Decision record
|
|
6
|
+
|
|
7
|
+
**Authorization Server: WorkOS AuthKit.** Rationale: explicit MCP authorization support including Dynamic Client Registration (the piece claude.ai requires), PKCE, JWKS-signed JWT access tokens, "Sign in with Google" as a login method, free tier covering early volume. Documented alternate: Clerk (§10). Self-hosted AS: deliberately out of scope (§11).
|
|
8
|
+
|
|
9
|
+
**Architecture rule that keeps the vendor swappable:** mcpscraper.dev is only the OAuth *protected resource*. All AS-specific behavior is isolated behind `src/api/oauth/` and three env vars. Swapping AS later = change env vars, re-test, no route changes.
|
|
10
|
+
|
|
11
|
+
**Two non-negotiable security rules:**
|
|
12
|
+
1. **No token passthrough.** The user's OAuth token is never forwarded anywhere. Internal REST calls from the MCP executor continue to use the resolved user's own `api_key` (the `users` row already carries it).
|
|
13
|
+
2. **Resource binding.** Tokens are accepted only when `aud` contains the canonical resource URI `https://mcpscraper.dev/mcp` (RFC 8707). A token minted for any other API is rejected even if the same AS signed it.
|
|
14
|
+
|
|
15
|
+
## The flow being implemented
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
claude.ai mcpscraper.dev WorkOS AuthKit
|
|
19
|
+
│ POST /mcp (no token) │ │
|
|
20
|
+
│←─ 401 + WWW-Authenticate ──────│ │
|
|
21
|
+
│ resource_metadata=… │ │
|
|
22
|
+
│ GET /.well-known/oauth-protected-resource ──→ JSON: AS issuer │
|
|
23
|
+
│ GET {issuer}/.well-known/oauth-authorization-server ─────────────→│
|
|
24
|
+
│ POST {issuer}/oauth2/register (Dynamic Client Registration) ────→│
|
|
25
|
+
│ browser: /oauth2/authorize + PKCE + resource=…/mcp (user logs in,│
|
|
26
|
+
│ via Google or email, approves) ─────────────────────────→│
|
|
27
|
+
│ POST {issuer}/oauth2/token ──────────────────────────────────────→│
|
|
28
|
+
│ POST /mcp Authorization: Bearer <JWT> ──→ verify JWKS + aud │
|
|
29
|
+
│ └→ resolve/provision user → tools │
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 1. New env vars — `src/api/env.ts` + `.env` + Vercel
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
OAUTH_ENABLED=true # master gate; absent/false = current behavior
|
|
36
|
+
OAUTH_ISSUER=https://<subdomain>.authkit.app # from WorkOS dashboard
|
|
37
|
+
OAUTH_JWKS_URL= # optional; defaults to ${OAUTH_ISSUER}/oauth2/jwks
|
|
38
|
+
OAUTH_AUDIENCE=https://mcpscraper.dev/mcp # canonical resource URI
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`env.ts`: add all four to `RequiredEnvSchema` as `z.string().optional()`. Do **not** add to `REQUIRED_VARS` (OAuth is optional infrastructure).
|
|
42
|
+
|
|
43
|
+
## 2. New dependency
|
|
44
|
+
|
|
45
|
+
`npm install jose` (runtime dep, not dev). Used for `createRemoteJWKSet` + `jwtVerify`. No other new deps.
|
|
46
|
+
|
|
47
|
+
## 3. New module: `src/api/oauth/`
|
|
48
|
+
|
|
49
|
+
### `oauth-config.ts`
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
export interface OAuthConfig { issuer: string; jwksUrl: string; audience: string }
|
|
53
|
+
export function oauthConfig(): OAuthConfig | null
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Returns `null` unless `OAUTH_ENABLED === 'true'` and `OAUTH_ISSUER` is set. `jwksUrl` defaults to `${issuer}/oauth2/jwks`. `audience` defaults to `https://mcpscraper.dev/mcp`.
|
|
57
|
+
|
|
58
|
+
### `verify-bearer.ts`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export interface OAuthIdentity { subject: string; email: string | null; scopes: string[] }
|
|
62
|
+
export async function verifyOAuthBearer(token: string): Promise<OAuthIdentity | null>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Implementation requirements:
|
|
66
|
+
- Module-level `createRemoteJWKSet(new URL(config.jwksUrl))` — jose caches keys internally; set `cooldownDuration: 30_000`, `cacheMaxAge: 3_600_000`.
|
|
67
|
+
- `jwtVerify(token, jwks, { issuer: config.issuer, audience: config.audience, algorithms: ['RS256', 'ES256'], clockTolerance: 60 })` — pinning `algorithms` rejects `alg: none` and downgrade attacks by construction.
|
|
68
|
+
- Extract `subject = payload.sub`, `email = payload.email ?? null` (WorkOS includes it), `scopes = (payload.scope ?? '').split(' ').filter(Boolean)`.
|
|
69
|
+
- Any verification error → return `null` (caller converts to 401). Never throw to the route.
|
|
70
|
+
|
|
71
|
+
### `resolve-user.ts`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
export async function resolveOAuthUser(identity: OAuthIdentity): Promise<User | null>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Resolution order (each step returns on hit):
|
|
78
|
+
1. `getUserByOauthSubject(identity.subject)` — fast path.
|
|
79
|
+
2. If `identity.email`: `getUserByEmail(email)` → if found, `linkOauthSubject(user.id, identity.subject)` and return (links existing dashboard accounts to their Google identity by verified email — WorkOS only emits verified emails).
|
|
80
|
+
3. Auto-provision: `createUser({ email, name: null })` (existing helper; generates `api_key`), then `grantSignupCredit(user.id)` (existing, idempotent), then `linkOauthSubject`. OAuth signups get the same 500-credit welcome grant as dashboard signups.
|
|
81
|
+
4. No email claim and no subject match → return `null` (401; we never create anonymous accounts).
|
|
82
|
+
|
|
83
|
+
## 4. DB changes — `src/api/db.ts`
|
|
84
|
+
|
|
85
|
+
Migration (append to the existing `migrate()` DDL):
|
|
86
|
+
|
|
87
|
+
```sql
|
|
88
|
+
ALTER TABLE users ADD COLUMN oauth_subject TEXT;
|
|
89
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_oauth_subject ON users(oauth_subject) WHERE oauth_subject IS NOT NULL;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Guard the `ALTER` with the existing duplicate-column try/catch pattern used elsewhere in `migrate()`. New helpers, same row-mapping style as `getUserByApiKey`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export async function getUserByOauthSubject(subject: string): Promise<User | undefined>
|
|
96
|
+
export async function getUserByEmail(email: string): Promise<User | undefined> // exists? reuse if so
|
|
97
|
+
export async function linkOauthSubject(userId: number | bigint, subject: string): Promise<void>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Add `oauth_subject: string | null` to the `User` type and every row-mapper that builds `User` (`rowToUser`, the sweep mapper in `credit-operations.ts`).
|
|
101
|
+
|
|
102
|
+
## 5. Protected Resource Metadata — new route
|
|
103
|
+
|
|
104
|
+
New file `src/api/oauth/metadata-routes.ts`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
export const oauthMetadataApp = new Hono()
|
|
108
|
+
oauthMetadataApp.get('/oauth-protected-resource', handler)
|
|
109
|
+
oauthMetadataApp.get('/oauth-protected-resource/mcp', handler) // path-scoped variant some clients request
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Handler returns 200 JSON (404 when `oauthConfig()` is null):
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"resource": "https://mcpscraper.dev/mcp",
|
|
117
|
+
"authorization_servers": ["<OAUTH_ISSUER>"],
|
|
118
|
+
"bearer_methods_supported": ["header"],
|
|
119
|
+
"scopes_supported": ["mcp:tools"],
|
|
120
|
+
"resource_name": "MCP Scraper"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Mount in `src/api/server.ts`: `app.route('/.well-known', oauthMetadataApp)`. Add to `vercel.json` rewrites: `{ "source": "/.well-known/:path*", "destination": "/api/index" }`.
|
|
125
|
+
|
|
126
|
+
## 6. `src/mcp/mcp-routes.ts` changes
|
|
127
|
+
|
|
128
|
+
### `mcpAuthError()` — upgrade the header
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
'WWW-Authenticate': `Bearer realm="mcp-scraper", resource_metadata="https://mcpscraper.dev/.well-known/oauth-protected-resource", error="invalid_token", error_description="Pass an MCP Scraper API key as x-api-key, or authenticate via OAuth"`
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The `resource_metadata` parameter is what tells claude.ai where to start discovery (RFC 9728 §5.1). The live HTTP test already asserts `/Bearer/`; extend it to assert `/resource_metadata/`.
|
|
135
|
+
|
|
136
|
+
### `requireMcpCallerKey()` → `requireMcpCaller()`
|
|
137
|
+
|
|
138
|
+
Return type changes from `string | Response` to `{ user: User; callerKey: string } | Response`. Logic:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const xApiKey = header('x-api-key')
|
|
142
|
+
const bearer = Authorization Bearer value
|
|
143
|
+
if (xApiKey) → getUserByApiKey(xApiKey) → { user, callerKey: xApiKey }
|
|
144
|
+
else if (bearer && bearer.split('.').length === 3 && oauthConfig()) {
|
|
145
|
+
const identity = await verifyOAuthBearer(bearer)
|
|
146
|
+
if (!identity) return mcpAuthError()
|
|
147
|
+
const user = await resolveOAuthUser(identity)
|
|
148
|
+
if (!user) return mcpAuthError()
|
|
149
|
+
return { user, callerKey: user.api_key } // executor bills/authorizes as this user
|
|
150
|
+
}
|
|
151
|
+
else if (bearer) → getUserByApiKey(bearer) → { user, callerKey: bearer } // existing sk_ Bearer path
|
|
152
|
+
else return mcpAuthError()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The three-dot heuristic routes JWTs to OAuth verification and `sk_*` keys to the existing lookup; order guarantees API keys never hit the JWT path. The handler body then uses `callerKey` exactly where it used the old return value — `new HttpMcpToolExecutor(baseUrl, callerKey)`. This is the no-passthrough rule made concrete: the OAuth token authenticates the session, but all internal REST work runs on the user's own API key.
|
|
156
|
+
|
|
157
|
+
## 7. WorkOS dashboard configuration (ops, no code)
|
|
158
|
+
|
|
159
|
+
1. Create WorkOS account → AuthKit → note the issuer `https://<subdomain>.authkit.app`.
|
|
160
|
+
2. Enable **Dynamic Client Registration** (Applications → Configuration). Without this claude.ai cannot register; this is the most commonly missed step.
|
|
161
|
+
3. Enable login methods: Google OAuth + email/password.
|
|
162
|
+
4. Confirm access tokens are JWTs and the JWKS endpoint responds: `curl ${issuer}/oauth2/jwks`.
|
|
163
|
+
5. Confirm AS metadata: `curl ${issuer}/.well-known/oauth-authorization-server` — must list `code` response type, PKCE `S256`, and the registration endpoint.
|
|
164
|
+
6. Set the four env vars in Vercel + `.env`, redeploy.
|
|
165
|
+
|
|
166
|
+
## 8. Tests
|
|
167
|
+
|
|
168
|
+
### `tests/unit/oauth-verify-bearer.test.ts`
|
|
169
|
+
|
|
170
|
+
Self-mint tokens with `jose.generateKeyPair('RS256')` + a local JWKS served via vitest mock of `createRemoteJWKSet` (inject key pair). Cases:
|
|
171
|
+
- valid token → identity with subject/email/scopes
|
|
172
|
+
- expired (`exp` in past, beyond 60s tolerance) → null
|
|
173
|
+
- wrong `aud` → null
|
|
174
|
+
- wrong `iss` → null
|
|
175
|
+
- tampered signature → null
|
|
176
|
+
- `alg: 'HS256'` (and `none`) → null
|
|
177
|
+
- missing `sub` → null
|
|
178
|
+
|
|
179
|
+
### `tests/unit/oauth-resolve-user.test.ts`
|
|
180
|
+
|
|
181
|
+
Mock the db helpers. Cases: subject fast-path; email-link path (asserts `linkOauthSubject` called once); auto-provision path (asserts `createUser` + `grantSignupCredit` + `linkOauthSubject`); no-email no-match → null.
|
|
182
|
+
|
|
183
|
+
### `tests/unit/oauth-metadata.test.ts`
|
|
184
|
+
|
|
185
|
+
With env set: 200 + exact JSON shape, both paths. Without: 404.
|
|
186
|
+
|
|
187
|
+
### Live (`tests/live/mcp/mcp-http-protocol.live.test.ts` additions)
|
|
188
|
+
|
|
189
|
+
- 401 `WWW-Authenticate` includes `resource_metadata` (extend existing test).
|
|
190
|
+
- `GET /.well-known/oauth-protected-resource` → 200 with `authorization_servers` non-empty (skip when `OAUTH_ISSUER` unset).
|
|
191
|
+
- Full token flow live test: gated on `OAUTH_TEST_TOKEN` env (a token minted manually via WorkOS test client); asserts `tools/list` succeeds with only the Bearer JWT.
|
|
192
|
+
|
|
193
|
+
### Manual E2E checklist (release gate)
|
|
194
|
+
|
|
195
|
+
In claude.ai → Settings → Connectors → Add custom connector → `https://mcpscraper.dev/mcp` → expect AuthKit login screen → Google login → tools appear → run `credits_info` → verify the ledger shows the call against the auto-provisioned/linked account.
|
|
196
|
+
|
|
197
|
+
## 9. Rollout phases
|
|
198
|
+
|
|
199
|
+
1. **Code, dark** — §§1–6 + unit tests, `OAUTH_ENABLED` unset in prod. Everything is inert; ship in a normal release. (Includes the harmless `WWW-Authenticate` upgrade — only adds a parameter.)
|
|
200
|
+
2. **WorkOS config + staging** — §7; flip `OAUTH_ENABLED=true` in prod (the gate makes this a config change, not a deploy); run live tests.
|
|
201
|
+
3. **Manual E2E** — §8 checklist with claude.ai.
|
|
202
|
+
4. **Docs surfaces** — README auth section; dashboard API-Key tab gets a "Connect via OAuth (claude.ai)" row in the client snippets (`public/app.jsx`, same component as the existing per-client snippets); `public/skills/mcp-scraper/skill.md` Authentication section gains one sentence.
|
|
203
|
+
5. **Version bump + release** per release-workflow (vercel first, then npm — though npm package is unaffected; stdio never uses OAuth).
|
|
204
|
+
|
|
205
|
+
## 10. Alternate AS: Clerk
|
|
206
|
+
|
|
207
|
+
Identical resource-server code (that's the point of §3's isolation). Differences: issuer is `https://<app>.clerk.accounts.dev` (or custom domain), JWKS at `${issuer}/.well-known/jwks.json` (set `OAUTH_JWKS_URL` explicitly), enable "Dynamic client registration" in Clerk's OAuth applications settings, and confirm Clerk session tokens carry `aud` — if Clerk's token template needs a custom claim for audience, configure a JWT template named `mcp` with `aud: "https://mcpscraper.dev/mcp"`.
|
|
208
|
+
|
|
209
|
+
## 11. Explicitly out of scope
|
|
210
|
+
|
|
211
|
+
- Self-hosted authorization server (login UI, `/oauth2/token`, DCR storage, consent screens on Turso). Feasible later precisely because §3 isolates the AS contract; revisit only if vendor cost or control becomes a problem.
|
|
212
|
+
- Scope-based tool authorization (per-tool scopes like `mcp:harvest`). Phase-2 candidate; `scopes` is already plumbed through `OAuthIdentity` so the data is there.
|
|
213
|
+
- OAuth for the raw REST API (non-MCP). API keys remain the REST contract.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for MCP Scraper web intelligence tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"bin": {
|
|
17
17
|
"paa-harvest": "dist/bin/paa-harvest.js",
|
|
18
18
|
"paa-api": "dist/bin/api-server.js",
|
|
19
|
-
"mcp-scraper": "dist/bin/mcp-stdio-server.js"
|
|
19
|
+
"mcp-scraper": "dist/bin/mcp-stdio-server.js",
|
|
20
|
+
"browser-agent": "dist/bin/browser-agent-stdio-server.js"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|