@fuul/mcp-server 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.
Files changed (74) hide show
  1. package/README.md +218 -0
  2. package/dist/affiliate-portal/affiliate-portal-queries.d.ts +15 -0
  3. package/dist/affiliate-portal/affiliate-portal-queries.d.ts.map +1 -0
  4. package/dist/affiliate-portal/affiliate-portal-queries.js +35 -0
  5. package/dist/affiliate-portal/affiliate-portal-queries.js.map +1 -0
  6. package/dist/agent/write-confirmation.d.ts +20 -0
  7. package/dist/agent/write-confirmation.d.ts.map +1 -0
  8. package/dist/agent/write-confirmation.js +34 -0
  9. package/dist/agent/write-confirmation.js.map +1 -0
  10. package/dist/auth/oauth-client.d.ts +13 -0
  11. package/dist/auth/oauth-client.d.ts.map +1 -0
  12. package/dist/auth/oauth-client.js +185 -0
  13. package/dist/auth/oauth-client.js.map +1 -0
  14. package/dist/auth/pkce.d.ts +5 -0
  15. package/dist/auth/pkce.d.ts.map +1 -0
  16. package/dist/auth/pkce.js +12 -0
  17. package/dist/auth/pkce.js.map +1 -0
  18. package/dist/auth/token-store.d.ts +7 -0
  19. package/dist/auth/token-store.d.ts.map +1 -0
  20. package/dist/auth/token-store.js +44 -0
  21. package/dist/auth/token-store.js.map +1 -0
  22. package/dist/auth/types.d.ts +13 -0
  23. package/dist/auth/types.d.ts.map +1 -0
  24. package/dist/auth/types.js +2 -0
  25. package/dist/auth/types.js.map +1 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +67 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/config/env.d.ts +10 -0
  31. package/dist/config/env.d.ts.map +1 -0
  32. package/dist/config/env.js +30 -0
  33. package/dist/config/env.js.map +1 -0
  34. package/dist/http/fuul-api-client.d.ts +51 -0
  35. package/dist/http/fuul-api-client.d.ts.map +1 -0
  36. package/dist/http/fuul-api-client.js +161 -0
  37. package/dist/http/fuul-api-client.js.map +1 -0
  38. package/dist/http/nest-query.d.ts +6 -0
  39. package/dist/http/nest-query.d.ts.map +1 -0
  40. package/dist/http/nest-query.js +24 -0
  41. package/dist/http/nest-query.js.map +1 -0
  42. package/dist/http/retry-after.d.ts +8 -0
  43. package/dist/http/retry-after.d.ts.map +1 -0
  44. package/dist/http/retry-after.js +28 -0
  45. package/dist/http/retry-after.js.map +1 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +366 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/metadata/metadata-service.d.ts +11 -0
  51. package/dist/metadata/metadata-service.d.ts.map +1 -0
  52. package/dist/metadata/metadata-service.js +71 -0
  53. package/dist/metadata/metadata-service.js.map +1 -0
  54. package/dist/payouts/payout-batch-handlers.d.ts +4 -0
  55. package/dist/payouts/payout-batch-handlers.d.ts.map +1 -0
  56. package/dist/payouts/payout-batch-handlers.js +34 -0
  57. package/dist/payouts/payout-batch-handlers.js.map +1 -0
  58. package/dist/tools/tool-descriptions.d.ts +25 -0
  59. package/dist/tools/tool-descriptions.d.ts.map +1 -0
  60. package/dist/tools/tool-descriptions.js +40 -0
  61. package/dist/tools/tool-descriptions.js.map +1 -0
  62. package/dist/tools/tool-schemas.d.ts +35 -0
  63. package/dist/tools/tool-schemas.d.ts.map +1 -0
  64. package/dist/tools/tool-schemas.js +145 -0
  65. package/dist/tools/tool-schemas.js.map +1 -0
  66. package/dist/util/compact-query.d.ts +3 -0
  67. package/dist/util/compact-query.d.ts.map +1 -0
  68. package/dist/util/compact-query.js +5 -0
  69. package/dist/util/compact-query.js.map +1 -0
  70. package/dist/util/with-timeout.d.ts +12 -0
  71. package/dist/util/with-timeout.d.ts.map +1 -0
  72. package/dist/util/with-timeout.js +35 -0
  73. package/dist/util/with-timeout.js.map +1 -0
  74. package/package.json +73 -0
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # @fuul/mcp-server
2
+
3
+ Fuul [Model Context Protocol](https://modelcontextprotocol.io/) server: OAuth CLI (`fuul-mcp`), stdio MCP entry (`fuul-mcp-server`), metadata tools, project and affiliate analytics, incentive and payout operations (reads plus `dry_run` / `confirmed` writes), and rate-limit-aware errors.
4
+
5
+ ## Documentation
6
+
7
+ | Resource | Purpose |
8
+ | -------- | ------- |
9
+ | [docs/README.md](docs/README.md) | Index of all docs in this repo |
10
+ | [docs/AGENTS.md](docs/AGENTS.md) | Tool ↔ HTTP map (audit, support, PR review) |
11
+ | [docs/mcp-phase2/CONSUMER.md](docs/mcp-phase2/CONSUMER.md) | Staging/production URLs, API expectations |
12
+ | [docs/mcp-phase2/tool-prompts.md](docs/mcp-phase2/tool-prompts.md) | Sample prompts for tooling and evals |
13
+ | [CHANGELOG.md](CHANGELOG.md) | Release notes |
14
+
15
+ ## Install (pick one path)
16
+
17
+ ### 1. Claude Code plugin (marketplace)
18
+
19
+ Adds the MCP server plus a **skill** that documents how to use Fuul tools.
20
+
21
+ In **Claude Code**:
22
+
23
+ ```text
24
+ /plugin marketplace add kuyen-labs/mcp_server
25
+ /plugin install fuul-mcp@fuul-mcp
26
+ ```
27
+
28
+ One-time OAuth in a terminal (same tokens the MCP uses):
29
+
30
+ ```bash
31
+ npx -y @fuul/mcp-server@latest fuul-mcp login
32
+ npx -y @fuul/mcp-server@latest fuul-mcp whoami
33
+ ```
34
+
35
+ Optional: set **staging** in the plugin’s user settings — `FUUL_API_BASE_URL` = `https://api.stg.fuul.xyz`. Default is production `https://api.fuul.xyz`.
36
+
37
+ Requires **`@fuul/mcp-server@0.2.0`** or newer on npm (for the `fuul-mcp-server` binary). After the first release with this version, `npx @latest` resolves correctly.
38
+
39
+ ### 2. npm / npx (any MCP client)
40
+
41
+ Use the published package without cloning:
42
+
43
+ | Command | Role |
44
+ | ------- | ---- |
45
+ | `npx -y @fuul/mcp-server@latest fuul-mcp-server` | Stdio MCP server (what clients spawn) |
46
+ | `npx -y @fuul/mcp-server@latest fuul-mcp login` | Browser OAuth; writes `~/.fuul/tokens.json` |
47
+ | `npx -y @fuul/mcp-server@latest fuul-mcp whoami` | `GET /api/v1/auth/user` |
48
+
49
+ Point your client at `fuul-mcp-server` with `cwd` optional; config can be passed via `env` (see **Configuration**).
50
+
51
+ ### 3. Clone (development)
52
+
53
+ ```bash
54
+ git clone https://github.com/kuyen-labs/mcp_server.git
55
+ cd mcp_server
56
+ npm ci
57
+ cp .env.example .env # optional; defaults match production Agent OAuth
58
+ npm run build
59
+ ```
60
+
61
+ Run the CLI from the repo:
62
+
63
+ ```bash
64
+ npm run cli -- login
65
+ npm run cli -- whoami
66
+ ```
67
+
68
+ Run the MCP server:
69
+
70
+ ```bash
71
+ npm start
72
+ # or: npm run dev
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ Environment variables are read from `process.env` and, when present, a **`.env` file in the current working directory** (`dotenv`). See [.env.example](.env.example).
78
+
79
+ | Variable | Purpose |
80
+ | -------- | ------- |
81
+ | `FUUL_API_BASE_URL` | API origin only, no trailing slash. Production: `https://api.fuul.xyz`. Staging: `https://api.stg.fuul.xyz`. |
82
+ | `FUUL_OAUTH_CLIENT_ID` | OAuth client id (default `fuul-agent`). |
83
+ | `FUUL_OAUTH_REDIRECT_URI` | Loopback callback (default `http://127.0.0.1:8765/callback`). |
84
+ | `FUUL_MCP_TOOL_TIMEOUT_MS` | Per-tool timeout in ms (default `90000`). |
85
+ | `FUUL_MCP_DEBUG` | Set to `1` or `true` for debug logging. |
86
+
87
+ **Note:** Values in `.env` are local dev convenience; they are **not** published in the npm package (`package.json` only ships `dist/`). OAuth **tokens** live in `~/.fuul/tokens.json`, not in `.env`.
88
+
89
+ ## MCP tool examples
90
+
91
+ Clients send JSON arguments; shapes match [docs/AGENTS.md](docs/AGENTS.md). Illustrative only — use real UUIDs from your tenant.
92
+
93
+ **Session**
94
+
95
+ - `ping` → `{}` (no API call)
96
+ - `whoami` → `{}` (requires login)
97
+
98
+ **Metadata**
99
+
100
+ - `list_chains`, `list_trigger_types`, `list_payout_schemas` → `{}`
101
+
102
+ **Projects**
103
+
104
+ - `list_projects` → `{ "page": 1, "query": "acme" }`
105
+ - `get_project` → `{ "project_id": "<uuid>" }`
106
+ - `list_incentives` / `get_incentive` / `get_trigger` → see tool descriptions in the client
107
+
108
+ **Affiliate analytics**
109
+
110
+ - `get_affiliate_portal_stats` → `project_id`, `user_identifier` (e.g. `evm:0x...`)
111
+ - `get_project_affiliate_total_stats` → `project_id`, optional `dateRange`, filters
112
+ - `get_project_affiliates_breakdown` → `project_id`, **`groupBy`** (`audience` \| `tier` \| `region` \| `status`)
113
+
114
+ **Writes (two steps: `dry_run: true` then `confirmed: true`)**
115
+
116
+ - `create_incentive_program`, `update_incentive_program`, `approve_payouts`, `reject_payouts`
117
+
118
+ ## Run and debug
119
+
120
+ ```bash
121
+ npm run build && npm start
122
+ ```
123
+
124
+ MCP Inspector:
125
+
126
+ ```bash
127
+ npm run build
128
+ npx @modelcontextprotocol/inspector node dist/index.js
129
+ ```
130
+
131
+ ## Client setup
132
+
133
+ ### Cursor
134
+
135
+ 1. Complete **Clone** or use **npx** so `dist/index.js` or `fuul-mcp-server` exists; run **`fuul-mcp login`** once.
136
+ 2. **Settings → MCP** (or user `mcp.json`): spawn stdio with `command` + `args`, and set **`cwd`** to a folder that contains `.env` if you use one.
137
+
138
+ Example:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "fuul": {
144
+ "command": "node",
145
+ "args": ["C:\\path\\to\\mcp_server\\dist\\index.js"],
146
+ "cwd": "C:\\path\\to\\mcp_server"
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ Or with npx:
153
+
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "fuul": {
158
+ "command": "npx",
159
+ "args": ["-y", "@fuul/mcp-server@latest", "fuul-mcp-server"],
160
+ "env": {
161
+ "FUUL_API_BASE_URL": "https://api.fuul.xyz"
162
+ }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Claude Desktop
169
+
170
+ Use the same `mcpServers` idea in the app’s developer config. See [Anthropic MCP docs](https://docs.anthropic.com/en/docs/mcp).
171
+
172
+ ### Claude Code (manual MCP, without the plugin)
173
+
174
+ Configure stdio per your Claude Code version: `node` + path to `dist/index.js`, or `npx` + `fuul-mcp-server` as above.
175
+
176
+ ## Repository layout
177
+
178
+ ```text
179
+ mcp_server/
180
+ ├── .claude-plugin/ # Claude Code marketplace manifest
181
+ │ └── marketplace.json
182
+ ├── plugins/
183
+ │ └── fuul-mcp/ # Plugin: MCP config + skill
184
+ │ ├── .claude-plugin/
185
+ │ │ └── plugin.json
186
+ │ ├── .mcp.json
187
+ │ └── skills/fuul/SKILL.md
188
+ ├── src/ # TypeScript source
189
+ ├── docs/ # Maintainer and integrator docs
190
+ ├── .github/workflows/ # ci.yml, publish.yml
191
+ └── dist/ # `npm run build` (gitignored)
192
+ ```
193
+
194
+ ## Requirements
195
+
196
+ - **Node.js** 18+
197
+ - A running **fuul-server** (staging or production) with **Agent OAuth** configured
198
+
199
+ ## CI and releases
200
+
201
+ On each push/PR to `main` / `master`, GitHub Actions runs **lint**, **test**, and **build** in parallel.
202
+
203
+ Publishing to npm is triggered by publishing a **GitHub Release** (see [.github/workflows/publish.yml](.github/workflows/publish.yml)); the repository needs an `NPM_TOKEN` secret.
204
+
205
+ ## Scripts
206
+
207
+ | Script | Description |
208
+ | ------ | ----------- |
209
+ | `npm run build` | Compile TypeScript → `dist/` |
210
+ | `npm start` | Run MCP server (`node dist/index.js`) |
211
+ | `npm run cli` | OAuth CLI via `tsx` (`src/cli.ts`) |
212
+ | `npm run dev` | MCP via `tsx` (`src/index.ts`) |
213
+ | `npm run lint` | ESLint on `src/` |
214
+ | `npm run test` | Vitest |
215
+
216
+ ## License
217
+
218
+ MIT — see `package.json`.
@@ -0,0 +1,15 @@
1
+ import type { GetAffiliatePortalStatsInput, GetProjectAffiliatesBreakdownInput, GetProjectAffiliateTotalStatsInput } from '../tools/tool-schemas.js';
2
+ /** Query object for GET /api/v1/projects/:projectId/affiliate-portal/stats */
3
+ export declare function affiliatePortalStatsQueryFromInput(input: GetAffiliatePortalStatsInput): Record<string, unknown>;
4
+ /** Query object for GET .../affiliate-portal/total-stats */
5
+ export declare function projectAffiliateTotalStatsQueryFromInput(input: GetProjectAffiliateTotalStatsInput): Record<string, unknown>;
6
+ /** Query object for GET .../affiliate-portal/global-breakdown */
7
+ export declare function projectAffiliatesBreakdownQueryFromInput(input: GetProjectAffiliatesBreakdownInput): Record<string, unknown>;
8
+ type AffiliatePortalSegment = 'stats' | 'total-stats' | 'global-breakdown';
9
+ /** Relative URL path with query string for affiliate-portal GET routes. */
10
+ export declare function buildAffiliatePortalGetPath(projectId: string, segment: AffiliatePortalSegment, query: Record<string, unknown>): string;
11
+ export declare function affiliatePortalStatsPath(input: GetAffiliatePortalStatsInput): string;
12
+ export declare function projectAffiliateTotalStatsPath(input: GetProjectAffiliateTotalStatsInput): string;
13
+ export declare function projectAffiliatesBreakdownPath(input: GetProjectAffiliatesBreakdownInput): string;
14
+ export {};
15
+ //# sourceMappingURL=affiliate-portal-queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affiliate-portal-queries.d.ts","sourceRoot":"","sources":["../../src/affiliate-portal/affiliate-portal-queries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,kCAAkC,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AASrJ,8EAA8E;AAC9E,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,4BAA4B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE/G;AAED,4DAA4D;AAC5D,wBAAgB,wCAAwC,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE3H;AAED,iEAAiE;AACjE,wBAAgB,wCAAwC,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE3H;AAED,KAAK,sBAAsB,GAAG,OAAO,GAAG,aAAa,GAAG,kBAAkB,CAAC;AAE3E,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAItI;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,4BAA4B,GAAG,MAAM,CAEpF;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAEhG;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAEhG"}
@@ -0,0 +1,35 @@
1
+ import { buildNestQueryString } from '../http/nest-query.js';
2
+ import { compactQuery } from '../util/compact-query.js';
3
+ function omitProjectId(input) {
4
+ const { project_id, ...rest } = input;
5
+ void project_id;
6
+ return rest;
7
+ }
8
+ /** Query object for GET /api/v1/projects/:projectId/affiliate-portal/stats */
9
+ export function affiliatePortalStatsQueryFromInput(input) {
10
+ return omitProjectId(input);
11
+ }
12
+ /** Query object for GET .../affiliate-portal/total-stats */
13
+ export function projectAffiliateTotalStatsQueryFromInput(input) {
14
+ return omitProjectId(input);
15
+ }
16
+ /** Query object for GET .../affiliate-portal/global-breakdown */
17
+ export function projectAffiliatesBreakdownQueryFromInput(input) {
18
+ return omitProjectId(input);
19
+ }
20
+ /** Relative URL path with query string for affiliate-portal GET routes. */
21
+ export function buildAffiliatePortalGetPath(projectId, segment, query) {
22
+ const base = `/api/v1/projects/${projectId}/affiliate-portal/${segment}`;
23
+ const q = buildNestQueryString(compactQuery(query));
24
+ return q ? `${base}?${q}` : base;
25
+ }
26
+ export function affiliatePortalStatsPath(input) {
27
+ return buildAffiliatePortalGetPath(input.project_id, 'stats', affiliatePortalStatsQueryFromInput(input));
28
+ }
29
+ export function projectAffiliateTotalStatsPath(input) {
30
+ return buildAffiliatePortalGetPath(input.project_id, 'total-stats', projectAffiliateTotalStatsQueryFromInput(input));
31
+ }
32
+ export function projectAffiliatesBreakdownPath(input) {
33
+ return buildAffiliatePortalGetPath(input.project_id, 'global-breakdown', projectAffiliatesBreakdownQueryFromInput(input));
34
+ }
35
+ //# sourceMappingURL=affiliate-portal-queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affiliate-portal-queries.js","sourceRoot":"","sources":["../../src/affiliate-portal/affiliate-portal-queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,SAAS,aAAa,CAAmC,KAAQ;IAC/D,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IACtC,KAAK,UAAU,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,kCAAkC,CAAC,KAAmC;IACpF,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,wCAAwC,CAAC,KAAyC;IAChG,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,wCAAwC,CAAC,KAAyC;IAChG,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAID,2EAA2E;AAC3E,MAAM,UAAU,2BAA2B,CAAC,SAAiB,EAAE,OAA+B,EAAE,KAA8B;IAC5H,MAAM,IAAI,GAAG,oBAAoB,SAAS,qBAAqB,OAAO,EAAE,CAAC;IACzE,MAAM,CAAC,GAAG,oBAAoB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAmC;IAC1E,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,kCAAkC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,KAAyC;IACtF,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,EAAE,wCAAwC,CAAC,KAAK,CAAC,CAAC,CAAC;AACvH,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,KAAyC;IACtF,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,kBAAkB,EAAE,wCAAwC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5H,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Standard input fragment for Phase 2 write tools. Extend with tool-specific fields.
4
+ * Flow: first call with dry_run: true (or omit confirmed) to get validation / preview;
5
+ * second call with confirmed: true to execute.
6
+ */
7
+ export declare const writeConfirmationFieldsSchema: any;
8
+ export type WriteConfirmationFields = z.infer<typeof writeConfirmationFieldsSchema>;
9
+ export declare class WriteNotConfirmedError extends Error {
10
+ readonly code: "WRITE_NOT_CONFIRMED";
11
+ constructor(message?: string);
12
+ }
13
+ /**
14
+ * Enforces the mandatory confirmation pattern for mutating tools.
15
+ * - dry_run: true → caller should return preview only (no throw).
16
+ * - confirmed: true → mutation allowed.
17
+ * - Otherwise → throws WriteNotConfirmedError.
18
+ */
19
+ export declare function assertWriteConfirmedOrDryRun(input: WriteConfirmationFields): void;
20
+ //# sourceMappingURL=write-confirmation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-confirmation.d.ts","sourceRoot":"","sources":["../../src/agent/write-confirmation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,KAGxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,qBAAqB,CAAU;gBAEnC,OAAO,SAAiG;CAKrH;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAQjF"}
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Standard input fragment for Phase 2 write tools. Extend with tool-specific fields.
4
+ * Flow: first call with dry_run: true (or omit confirmed) to get validation / preview;
5
+ * second call with confirmed: true to execute.
6
+ */
7
+ export const writeConfirmationFieldsSchema = z.object({
8
+ dry_run: z.boolean().optional().describe('If true, validate and return a preview only; no server mutation.'),
9
+ confirmed: z.boolean().optional().describe('Must be true to perform the mutation after reviewing dry_run output.'),
10
+ });
11
+ export class WriteNotConfirmedError extends Error {
12
+ code = 'WRITE_NOT_CONFIRMED';
13
+ constructor(message = 'Mutation blocked: use dry_run: true first to validate, then call again with confirmed: true.') {
14
+ super(message);
15
+ this.name = 'WriteNotConfirmedError';
16
+ Object.setPrototypeOf(this, new.target.prototype);
17
+ }
18
+ }
19
+ /**
20
+ * Enforces the mandatory confirmation pattern for mutating tools.
21
+ * - dry_run: true → caller should return preview only (no throw).
22
+ * - confirmed: true → mutation allowed.
23
+ * - Otherwise → throws WriteNotConfirmedError.
24
+ */
25
+ export function assertWriteConfirmedOrDryRun(input) {
26
+ if (input.dry_run === true) {
27
+ return;
28
+ }
29
+ if (input.confirmed === true) {
30
+ return;
31
+ }
32
+ throw new WriteNotConfirmedError();
33
+ }
34
+ //# sourceMappingURL=write-confirmation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-confirmation.js","sourceRoot":"","sources":["../../src/agent/write-confirmation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kEAAkE,CAAC;IAC5G,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;CACnH,CAAC,CAAC;AAIH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,qBAA8B,CAAC;IAE/C,YAAY,OAAO,GAAG,8FAA8F;QAClH,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAA8B;IACzE,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,MAAM,IAAI,sBAAsB,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type Env } from '../config/env.js';
2
+ import { TokenStore } from './token-store.js';
3
+ import type { StoredTokens } from './types.js';
4
+ export declare class OAuthClient {
5
+ private readonly env;
6
+ private readonly tokenStore;
7
+ constructor(env: Env, tokenStore: TokenStore);
8
+ private log;
9
+ private http;
10
+ login(): Promise<void>;
11
+ refreshFromStore(): Promise<StoredTokens | null>;
12
+ }
13
+ //# sourceMappingURL=oauth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoB,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAC;AAI9D,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,UAAU;gBADV,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,UAAU;IAGzC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,IAAI;IAUN,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDtB,gBAAgB,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAiBvD"}
@@ -0,0 +1,185 @@
1
+ import http from 'node:http';
2
+ import { URL } from 'node:url';
3
+ import axios from 'axios';
4
+ import open from 'open';
5
+ import { apiOriginFromEnv } from '../config/env.js';
6
+ import { createCodeChallengeS256, createCodeVerifier, createOAuthState } from './pkce.js';
7
+ const LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
8
+ export class OAuthClient {
9
+ env;
10
+ tokenStore;
11
+ constructor(env, tokenStore) {
12
+ this.env = env;
13
+ this.tokenStore = tokenStore;
14
+ }
15
+ log(...args) {
16
+ if (this.env.debug) {
17
+ console.error('[fuul-mcp]', ...args);
18
+ }
19
+ }
20
+ http() {
21
+ const baseURL = apiOriginFromEnv(this.env);
22
+ return axios.create({
23
+ baseURL,
24
+ timeout: 30_000,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ validateStatus: () => true,
27
+ });
28
+ }
29
+ async login() {
30
+ const origin = apiOriginFromEnv(this.env);
31
+ const verifier = createCodeVerifier();
32
+ const challenge = createCodeChallengeS256(verifier);
33
+ const state = createOAuthState();
34
+ const redirectUri = this.env.FUUL_OAUTH_REDIRECT_URI;
35
+ const { port, hostname, pathname } = parseRedirect(redirectUri);
36
+ const authorizeUrl = new URL(`${origin}/oauth/authorize`);
37
+ authorizeUrl.searchParams.set('response_type', 'code');
38
+ authorizeUrl.searchParams.set('client_id', this.env.FUUL_OAUTH_CLIENT_ID);
39
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
40
+ authorizeUrl.searchParams.set('state', state);
41
+ authorizeUrl.searchParams.set('code_challenge', challenge);
42
+ authorizeUrl.searchParams.set('code_challenge_method', 'S256');
43
+ const codePromise = captureAuthorizationCode({
44
+ port,
45
+ hostname,
46
+ pathname,
47
+ expectedState: state,
48
+ timeoutMs: LOGIN_TIMEOUT_MS,
49
+ log: this.log.bind(this),
50
+ });
51
+ this.log('Opening browser for login:', authorizeUrl.toString());
52
+ await open(authorizeUrl.toString());
53
+ const { code } = await codePromise;
54
+ const client = this.http();
55
+ const tokenRes = await client.post('/oauth/token', {
56
+ grant_type: 'authorization_code',
57
+ code,
58
+ redirect_uri: redirectUri,
59
+ client_id: this.env.FUUL_OAUTH_CLIENT_ID,
60
+ code_verifier: verifier,
61
+ });
62
+ if (tokenRes.status !== 200 || !tokenRes.data?.access_token) {
63
+ const body = tokenRes.data;
64
+ const msg = body?.error_description || `Token exchange failed (HTTP ${tokenRes.status})`;
65
+ throw new Error(msg);
66
+ }
67
+ const tokens = tokenResponseToStored(tokenRes.data);
68
+ await this.tokenStore.write(tokens);
69
+ console.log('Logged in. Tokens saved to ~/.fuul/tokens.json');
70
+ }
71
+ async refreshFromStore() {
72
+ const current = await this.tokenStore.read();
73
+ if (!current?.refresh_token) {
74
+ return null;
75
+ }
76
+ const client = this.http();
77
+ const tokenRes = await client.post('/oauth/token', {
78
+ grant_type: 'refresh_token',
79
+ refresh_token: current.refresh_token,
80
+ });
81
+ if (tokenRes.status !== 200 || !tokenRes.data?.access_token) {
82
+ return null;
83
+ }
84
+ const tokens = tokenResponseToStored(tokenRes.data);
85
+ await this.tokenStore.write(tokens);
86
+ return tokens;
87
+ }
88
+ }
89
+ function tokenResponseToStored(data) {
90
+ const expires_at_ms = Date.now() + Math.max(0, data.expires_in) * 1000;
91
+ return {
92
+ access_token: data.access_token,
93
+ refresh_token: data.refresh_token,
94
+ expires_at_ms,
95
+ };
96
+ }
97
+ function parseRedirect(redirectUri) {
98
+ const u = new URL(redirectUri);
99
+ const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
100
+ const hostname = u.hostname;
101
+ const pathname = u.pathname || '/';
102
+ return { port, hostname, pathname };
103
+ }
104
+ function captureAuthorizationCode(opts) {
105
+ const { port, hostname, pathname, expectedState, timeoutMs, log } = opts;
106
+ return new Promise((resolve, reject) => {
107
+ let settled = false;
108
+ const timer = setTimeout(() => {
109
+ if (!settled) {
110
+ settled = true;
111
+ server.close();
112
+ reject(new Error('Login timed out waiting for browser redirect'));
113
+ }
114
+ }, timeoutMs);
115
+ const server = http.createServer((req, res) => {
116
+ void (async () => {
117
+ try {
118
+ const hostUrl = `http://${req.headers.host ?? `${hostname}:${port}`}`;
119
+ const url = new URL(req.url ?? '/', hostUrl);
120
+ if (url.pathname !== pathname) {
121
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
122
+ res.end('Not found');
123
+ return;
124
+ }
125
+ const oauthError = url.searchParams.get('error');
126
+ if (oauthError) {
127
+ const desc = url.searchParams.get('error_description') ?? oauthError;
128
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
129
+ res.end(`<p>Authorization error: ${escapeHtml(desc)}</p>`);
130
+ if (!settled) {
131
+ settled = true;
132
+ clearTimeout(timer);
133
+ server.close();
134
+ reject(new Error(desc));
135
+ }
136
+ return;
137
+ }
138
+ const code = url.searchParams.get('code');
139
+ const state = url.searchParams.get('state');
140
+ if (!code || state !== expectedState) {
141
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
142
+ res.end('<p>Invalid callback (missing code or state mismatch).</p>');
143
+ if (!settled) {
144
+ settled = true;
145
+ clearTimeout(timer);
146
+ server.close();
147
+ reject(new Error('Invalid OAuth callback'));
148
+ }
149
+ return;
150
+ }
151
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
152
+ res.end('<p>Login complete. You can close this window.</p>');
153
+ if (!settled) {
154
+ settled = true;
155
+ clearTimeout(timer);
156
+ server.close(() => resolve({ code }));
157
+ }
158
+ }
159
+ catch (e) {
160
+ log(e);
161
+ if (!settled) {
162
+ settled = true;
163
+ clearTimeout(timer);
164
+ server.close();
165
+ reject(e instanceof Error ? e : new Error(String(e)));
166
+ }
167
+ }
168
+ })();
169
+ });
170
+ server.on('error', (e) => {
171
+ if (!settled) {
172
+ settled = true;
173
+ clearTimeout(timer);
174
+ reject(e);
175
+ }
176
+ });
177
+ server.listen(port, hostname, () => {
178
+ log(`Listening for OAuth callback on http://${hostname}:${port}${pathname}`);
179
+ });
180
+ });
181
+ }
182
+ function escapeHtml(s) {
183
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
184
+ }
185
+ //# sourceMappingURL=oauth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-client.js","sourceRoot":"","sources":["../../src/auth/oauth-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** RFC 7636 code verifier (43–128 characters). */
2
+ export declare function createCodeVerifier(): string;
3
+ export declare function createCodeChallengeS256(verifier: string): string;
4
+ export declare function createOAuthState(): string;
5
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA,kDAAkD;AAClD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
@@ -0,0 +1,12 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ /** RFC 7636 code verifier (43–128 characters). */
3
+ export function createCodeVerifier() {
4
+ return randomBytes(32).toString('base64url');
5
+ }
6
+ export function createCodeChallengeS256(verifier) {
7
+ return createHash('sha256').update(verifier).digest('base64url');
8
+ }
9
+ export function createOAuthState() {
10
+ return randomBytes(16).toString('base64url');
11
+ }
12
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,kDAAkD;AAClD,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { StoredTokens } from './types.js';
2
+ export declare class TokenStore {
3
+ read(): Promise<StoredTokens | null>;
4
+ write(tokens: StoredTokens): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
7
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C,qBAAa,UAAU;IACf,IAAI,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAapC,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO7B"}
@@ -0,0 +1,44 @@
1
+ import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const DIR_NAME = '.fuul';
5
+ const FILE_NAME = 'tokens.json';
6
+ function tokenFilePath() {
7
+ return join(homedir(), DIR_NAME, FILE_NAME);
8
+ }
9
+ export class TokenStore {
10
+ async read() {
11
+ try {
12
+ const raw = await readFile(tokenFilePath(), 'utf8');
13
+ const data = JSON.parse(raw);
14
+ if (!data.access_token || !data.refresh_token || typeof data.expires_at_ms !== 'number') {
15
+ return null;
16
+ }
17
+ return data;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ async write(tokens) {
24
+ const dir = join(homedir(), DIR_NAME);
25
+ await mkdir(dir, { recursive: true });
26
+ const path = tokenFilePath();
27
+ await writeFile(path, `${JSON.stringify(tokens, null, 2)}\n`, 'utf8');
28
+ try {
29
+ await chmod(path, 0o600);
30
+ }
31
+ catch {
32
+ // Windows may ignore mode; ignore errors.
33
+ }
34
+ }
35
+ async clear() {
36
+ try {
37
+ await unlink(tokenFilePath());
38
+ }
39
+ catch {
40
+ // ignore if missing
41
+ }
42
+ }
43
+ }
44
+ //# sourceMappingURL=token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,MAAM,QAAQ,GAAG,OAAO,CAAC;AACzB,MAAM,SAAS,GAAG,aAAa,CAAC;AAEhC,SAAS,aAAa;IACpB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,CAAC,CAAC;YACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;YAC7C,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;gBACxF,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,MAAoB;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;QACtC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;QAC7B,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtE,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;IACH,CAAC;CACF"}