@orchyn/mcp 1.1.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/LICENSE +21 -0
- package/README.md +285 -0
- package/dist/auth.js +170 -0
- package/dist/config.js +54 -0
- package/dist/index.js +564 -0
- package/dist/oauth.js +303 -0
- package/dist/orchyn.js +164 -0
- package/dist/video.js +129 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orchyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# @orchyn/mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for [orchyn](https://orchyn.com) — fetch,
|
|
4
|
+
discover and understand social posts (TikTok, Instagram, YouTube, X) with your
|
|
5
|
+
orchyn account and orchyn credits.
|
|
6
|
+
|
|
7
|
+
## Install (one link)
|
|
8
|
+
|
|
9
|
+
**Claude Code** — register the marketplace, then install the plugin:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add orchynX/mcp
|
|
13
|
+
/plugin install orchyn@orchyn
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Claude Code / CLI without the plugin**:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
claude mcp add orchyn --user -- npx -y @orchyn/mcp
|
|
20
|
+
npx @orchyn/mcp login # one-time sign-in (Google)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Cursor / any stdio MCP client** (`claude_desktop_config.json`, `.mcp.json`, …):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"orchyn": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "@orchyn/mcp"],
|
|
31
|
+
"env": { "ORCHYN_BASE_URL": "https://api.orchyn.com" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Credits | Description |
|
|
40
|
+
|------|---------|-------------|
|
|
41
|
+
| `analyze_video` | first free | Start an AI analysis of a TikTok/Instagram/YouTube video; polls until done and returns the full analysis. |
|
|
42
|
+
| `get_social_media` | 1 | Fetch a post's media from a URL: contentType, title, caption, author, stats, direct media URLs + inline thumbnail image. |
|
|
43
|
+
| `discover_social_videos` | 2 | Find recent videos for a niche (YouTube search; TikTok/Instagram via Apify). |
|
|
44
|
+
| `understand_social_post` | 10 | Import a post URL AND analyze it with multimodal AI over the actual video/images: summary, hook strength, viral triggers, format breakdown, variation ideas. |
|
|
45
|
+
|
|
46
|
+
All tools require a connected orchyn account and are billed against your
|
|
47
|
+
orchyn credit balance (`POST /billing/mcp-credits/checkout` tops up).
|
|
48
|
+
|
|
49
|
+
The server starts the analysis on the orchyn backend, polls until the analysis
|
|
50
|
+
finishes, and returns the full result (analysis, job metadata, cost) as JSON.
|
|
51
|
+
|
|
52
|
+
## Prerequisites
|
|
53
|
+
|
|
54
|
+
- Node.js >= 18 (tested on Node 22)
|
|
55
|
+
- An orchyn account (created automatically on first sign-in — Google sign-in
|
|
56
|
+
via `orchyn-mcp login`; the dashboard is **not** required: the server
|
|
57
|
+
auto-creates a default workspace + app for new accounts)
|
|
58
|
+
- Access to an orchyn server (default `http://localhost:8080`, see `ORCHYN_BASE_URL`)
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
npm run build
|
|
65
|
+
|
|
66
|
+
# 1. Sign in with your orchyn account (Google sign-in opens in your browser)
|
|
67
|
+
npx @orchyn/mcp login
|
|
68
|
+
# or with email/password:
|
|
69
|
+
npx @orchyn/mcp login --email you@example.com --password '...'
|
|
70
|
+
|
|
71
|
+
# 2. Run the server
|
|
72
|
+
npx @orchyn/mcp # stdio (default; for Claude Desktop / Cursor)
|
|
73
|
+
npx @orchyn/mcp --http # remote HTTP with OAuth (for OpenAI Agents SDK)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`login` stores your orchyn tokens in `~/.config/orchyn-mcp/credentials.json`
|
|
77
|
+
(mode `0600`). If your browser cannot be opened automatically, copy the URL it
|
|
78
|
+
prints into a browser manually.
|
|
79
|
+
|
|
80
|
+
## Usage in Claude Desktop
|
|
81
|
+
|
|
82
|
+
After `npx @orchyn/mcp login`, add to `claude_desktop_config.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"orchyn": {
|
|
88
|
+
"command": "npx",
|
|
89
|
+
"args": ["-y", "@orchyn/mcp"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Tokens are resolved from the credentials file written by `login` (or from
|
|
96
|
+
`ORCHYN_ACCESS_TOKEN`). If your client does not inherit your shell environment,
|
|
97
|
+
set the env vars explicitly:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"orchyn": {
|
|
103
|
+
"command": "npx",
|
|
104
|
+
"args": ["-y", "@orchyn/mcp"],
|
|
105
|
+
"env": {
|
|
106
|
+
"ORCHYN_BASE_URL": "http://localhost:8080"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Usage in Cursor
|
|
114
|
+
|
|
115
|
+
In `.mcp.json` (project root) or `~/.cursor/mcp.json`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"orchyn": {
|
|
121
|
+
"command": "npx",
|
|
122
|
+
"args": ["-y", "@orchyn/mcp"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
After adding the server, run `npx @orchyn/mcp login` in your terminal — Cursor
|
|
129
|
+
spawns the server with your environment, so it picks up the stored credentials.
|
|
130
|
+
|
|
131
|
+
For remote/HTTP usage, Cursor can connect to the OAuth-enabled HTTP mode with
|
|
132
|
+
`npx @orchyn/mcp --http` running, pointing the server URL at
|
|
133
|
+
`http://localhost:3457/mcp`. Remote clients (including Cursor) discover the
|
|
134
|
+
OAuth endpoints from
|
|
135
|
+
`http://localhost:3457/.well-known/oauth-authorization-server`, open the
|
|
136
|
+
"Sign in with Google" page, and store the resulting access token.
|
|
137
|
+
|
|
138
|
+
## Usage with OpenAI Agents SDK
|
|
139
|
+
|
|
140
|
+
Start the HTTP transport:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx @orchyn/mcp --http --port 3457
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Python:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from agents import Agent, Runner
|
|
150
|
+
from agents.mcp import RemoteMCPClient
|
|
151
|
+
|
|
152
|
+
async def main():
|
|
153
|
+
async with RemoteMCPClient(
|
|
154
|
+
url="http://localhost:3457/mcp",
|
|
155
|
+
auth_provider="oidc", # OAuth flow opens your browser once
|
|
156
|
+
) as client:
|
|
157
|
+
agent = Agent(name="orchyn", mcp_servers=[client])
|
|
158
|
+
result = await Runner.run(
|
|
159
|
+
agent,
|
|
160
|
+
"Analyze this video: https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
161
|
+
)
|
|
162
|
+
print(result.final_output)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
TypeScript (Agents SDK):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { Agent } from "agents";
|
|
169
|
+
import { RemoteMCPClient } from "agents/mcp/client";
|
|
170
|
+
|
|
171
|
+
const client = new RemoteMCPClient({
|
|
172
|
+
url: "http://localhost:3457/mcp",
|
|
173
|
+
authProvider: "oidc", // opens the browser for the OAuth flow
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const agent = new Agent({
|
|
177
|
+
name: "orchyn",
|
|
178
|
+
mcpServers: [client],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await agent.run(
|
|
182
|
+
"Analyze this video: https://vm.tiktok.com/abc123/",
|
|
183
|
+
);
|
|
184
|
+
console.log(result.output);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For a local stdio process with the Agents SDK, use `StdioMCPClient` (Python:
|
|
188
|
+
`StdioMCPClient(command="npx", args=["orchyn-mcp"])`).
|
|
189
|
+
|
|
190
|
+
## Command line
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
orchyn-mcp Start in stdio mode (default transport)
|
|
194
|
+
orchyn-mcp --stdio Same as above
|
|
195
|
+
orchyn-mcp --http [--port N] Start the remote HTTP transport with OAuth (default port 3457)
|
|
196
|
+
orchyn-mcp login Sign in to orchyn via Google in your browser
|
|
197
|
+
orchyn-mcp login --email ... --password ... Password login
|
|
198
|
+
orchyn-mcp --help Show help
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Environment variables
|
|
202
|
+
|
|
203
|
+
| Variable | Default | Description |
|
|
204
|
+
|----------|---------|-------------|
|
|
205
|
+
| `ORCHYN_BASE_URL` | `http://localhost:8080` | orchyn server base URL (trailing slash stripped) |
|
|
206
|
+
| `ORCHYN_ACCESS_TOKEN` | — | orchyn JWT access token; takes priority over the credentials file |
|
|
207
|
+
| `ORCHYN_CREDENTIALS_FILE` | `~/.config/orchyn-mcp/credentials.json` | token store path |
|
|
208
|
+
| `ORCHYN_PUBLIC_URL` | `http://localhost:3457` | public base URL advertised in OAuth metadata (HTTP mode) |
|
|
209
|
+
| `ORCHYN_PORT` | `3457` | port for `--http` and `login` |
|
|
210
|
+
| `ORCHYN_TRANSPORT` | `stdio` | `stdio` or `http` (same as `--http`) |
|
|
211
|
+
|
|
212
|
+
## How authentication works
|
|
213
|
+
|
|
214
|
+
**stdio mode** (Claude Desktop, Cursor): the server uses the token from
|
|
215
|
+
`ORCHYN_ACCESS_TOKEN` or the credentials file written by `login`. If the token
|
|
216
|
+
is expired it is automatically refreshed with the stored refresh token, and if
|
|
217
|
+
the orchyn API returns `401` the request is retried once after a refresh.
|
|
218
|
+
|
|
219
|
+
**HTTP mode** (OpenAI Agents SDK, remote clients): the server runs its own
|
|
220
|
+
OAuth 2.0 authorization server (Authorization Code + PKCE S256, public client,
|
|
221
|
+
per the MCP 2025-03-26 spec):
|
|
222
|
+
|
|
223
|
+
- `GET /.well-known/oauth-authorization-server` — metadata
|
|
224
|
+
- `GET /authorize` — validates the request (loopback or https redirect URIs)
|
|
225
|
+
and forwards the browser to orchyn's Google sign-in
|
|
226
|
+
- `GET /oauth/callback` — our own loopback callback; exchanges orchyn's
|
|
227
|
+
completion code for orchyn JWTs and redirects back to the MCP client with a
|
|
228
|
+
one-time code
|
|
229
|
+
- `POST /token` — verifies PKCE and issues an opaque Bearer token bound to the
|
|
230
|
+
orchyn session (valid 1 hour)
|
|
231
|
+
- every MCP RPC validates the Bearer token against the session map
|
|
232
|
+
|
|
233
|
+
## Supported video URLs
|
|
234
|
+
|
|
235
|
+
- TikTok: `tiktok.com/*`, `vm.tiktok.com/*` (and `www.`/`m.` subdomains)
|
|
236
|
+
- Instagram: `instagram.com/*` (reels, posts), `instagr.am/*`
|
|
237
|
+
- YouTube: `youtube.com/*` (including `/shorts/`), `youtu.be/*`, `m.youtube.com/*`
|
|
238
|
+
|
|
239
|
+
Other hosts are rejected by the tool.
|
|
240
|
+
|
|
241
|
+
## Troubleshooting
|
|
242
|
+
|
|
243
|
+
- **`Not authenticated with orchyn` / 401**: run `npx @orchyn/mcp login` or set
|
|
244
|
+
`ORCHYN_ACCESS_TOKEN`.
|
|
245
|
+
- **402 paywall (`Analysis blocked`)**: your orchyn account has no credits
|
|
246
|
+
left. The error includes `reason`, `used`/`max`, and `cost`. Top up or check
|
|
247
|
+
your usage in the orchyn dashboard. The first analysis is covered by the
|
|
248
|
+
free grant.
|
|
249
|
+
- **Expired refresh token**: the stored refresh token was rejected by the
|
|
250
|
+
orchyn server. Run `npx @orchyn/mcp login` again to re-authenticate.
|
|
251
|
+
- **`Could not reach the orchyn server`**: `ORCHYN_BASE_URL` is unreachable or
|
|
252
|
+
wrong.
|
|
253
|
+
- **Client shows "Bad Request" or connection errors in HTTP mode**: make sure
|
|
254
|
+
the port matches `ORCHYN_PUBLIC_URL` and that the client fetched a token
|
|
255
|
+
first (the OAuth flow must complete once in your browser).
|
|
256
|
+
|
|
257
|
+
## Platform submissions
|
|
258
|
+
|
|
259
|
+
See [docs/SUBMISSION.md](docs/SUBMISSION.md) for ready-to-paste configs and the submission
|
|
260
|
+
package for **Claude Desktop**, **Cursor**, and **OpenAI Agents SDK**.
|
|
261
|
+
|
|
262
|
+
## Security notes
|
|
263
|
+
|
|
264
|
+
- The credentials file is written with mode `0600` and the directory with
|
|
265
|
+
`0700`.
|
|
266
|
+
- OAuth redirect URIs are restricted to loopback (`http://localhost`,
|
|
267
|
+
`http://127.0.0.1`, `http://[::1]`) or `https://` URLs; the `/authorize`
|
|
268
|
+
endpoint requires PKCE (`S256`).
|
|
269
|
+
- Authorization codes and PKCE challenges are one-time use and short-lived
|
|
270
|
+
(in-memory).
|
|
271
|
+
- Access tokens are opaque, random, and bound to the in-memory session map;
|
|
272
|
+
they expire after 1 hour. Restarting the server invalidates all sessions.
|
|
273
|
+
- Never share your credentials file or `ORCHYN_ACCESS_TOKEN`.
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npm install
|
|
279
|
+
npm run build # tsc
|
|
280
|
+
npm test # vitest (54 unit tests, mocked fetch — no network)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage + resolution.
|
|
3
|
+
*
|
|
4
|
+
* Priority: env ORCHYN_ACCESS_TOKEN > credentials file (auto-refresh when
|
|
5
|
+
* expired) > per-session tokens (HTTP OAuth mode only).
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { OrchynClient, OrchynError } from "./orchyn.js";
|
|
10
|
+
const REFRESH_BEFORE_EXPIRY_MS = 30_000;
|
|
11
|
+
export class OrchynAuthError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "OrchynAuthError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isTokenExpired(store, nowMs = Date.now()) {
|
|
18
|
+
if (!store.expiresIn || !store.fetchedAt)
|
|
19
|
+
return false;
|
|
20
|
+
const expiresAtMs = store.fetchedAt + store.expiresIn * 1000;
|
|
21
|
+
return nowMs >= expiresAtMs - REFRESH_BEFORE_EXPIRY_MS;
|
|
22
|
+
}
|
|
23
|
+
export class AuthManager {
|
|
24
|
+
baseUrl;
|
|
25
|
+
credentialsFile;
|
|
26
|
+
client;
|
|
27
|
+
lastRefreshToken;
|
|
28
|
+
lastSource;
|
|
29
|
+
constructor(baseUrl, credentialsFile) {
|
|
30
|
+
this.baseUrl = baseUrl;
|
|
31
|
+
this.credentialsFile = credentialsFile;
|
|
32
|
+
this.client = new OrchynClient(baseUrl, {
|
|
33
|
+
getAccessToken: async () => undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
getCredentialsFile() {
|
|
37
|
+
return this.credentialsFile;
|
|
38
|
+
}
|
|
39
|
+
async loadStore() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await fs.promises.readFile(this.credentialsFile, "utf8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (parsed && typeof parsed.accessToken === "string") {
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async saveStore(store) {
|
|
53
|
+
const dir = path.dirname(this.credentialsFile);
|
|
54
|
+
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
55
|
+
const tmp = `${this.credentialsFile}.tmp`;
|
|
56
|
+
await fs.promises.writeFile(tmp, JSON.stringify(store, null, 2), {
|
|
57
|
+
mode: 0o600,
|
|
58
|
+
});
|
|
59
|
+
await fs.promises.chmod(tmp, 0o600);
|
|
60
|
+
await fs.promises.rename(tmp, this.credentialsFile);
|
|
61
|
+
await fs.promises.chmod(this.credentialsFile, 0o600);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the current access token for a request. `session` provides the
|
|
65
|
+
* per-session tokens issued via our own OAuth /token endpoint (HTTP mode).
|
|
66
|
+
*/
|
|
67
|
+
async getAccessToken(session) {
|
|
68
|
+
const envToken = process.env.ORCHYN_ACCESS_TOKEN;
|
|
69
|
+
if (envToken) {
|
|
70
|
+
this.lastSource = "env";
|
|
71
|
+
this.lastRefreshToken = undefined;
|
|
72
|
+
return envToken;
|
|
73
|
+
}
|
|
74
|
+
const store = await this.loadStore();
|
|
75
|
+
if (store?.accessToken && !isTokenExpired(store)) {
|
|
76
|
+
this.lastSource = "file";
|
|
77
|
+
this.lastRefreshToken = store.refreshToken;
|
|
78
|
+
return store.accessToken;
|
|
79
|
+
}
|
|
80
|
+
if (store?.refreshToken) {
|
|
81
|
+
try {
|
|
82
|
+
const refreshed = await this.refresh(store.refreshToken);
|
|
83
|
+
return refreshed;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// Fall through to session/env resolution; the file refresh will be
|
|
87
|
+
// retried lazily on the next request.
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
process.stderr.write(`[orchyn-mcp] warning: could not refresh stored token: ${msg}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (session?.accessToken) {
|
|
93
|
+
this.lastSource = "session";
|
|
94
|
+
this.lastRefreshToken = session.refreshToken;
|
|
95
|
+
return session.accessToken;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Attempts to refresh using the refresh token of the source that supplied
|
|
101
|
+
* the current token. Returns true when a new token is available.
|
|
102
|
+
*/
|
|
103
|
+
async onUnauthorized(session) {
|
|
104
|
+
let refreshToken = this.lastRefreshToken;
|
|
105
|
+
if (this.lastSource === "session" && session?.refreshToken) {
|
|
106
|
+
refreshToken = session.refreshToken;
|
|
107
|
+
}
|
|
108
|
+
if (!refreshToken)
|
|
109
|
+
return false;
|
|
110
|
+
try {
|
|
111
|
+
await this.refresh(refreshToken);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Refreshes with the given refresh token, persists, and returns the new access token. */
|
|
119
|
+
async refresh(refreshToken) {
|
|
120
|
+
const session = await this.client.refresh(refreshToken);
|
|
121
|
+
if (!session.accessToken) {
|
|
122
|
+
throw new OrchynError(500, "Refresh succeeded but returned no access token.");
|
|
123
|
+
}
|
|
124
|
+
const store = {
|
|
125
|
+
accessToken: session.accessToken,
|
|
126
|
+
refreshToken: session.refreshToken ?? refreshToken,
|
|
127
|
+
expiresIn: session.expiresIn,
|
|
128
|
+
fetchedAt: Date.now(),
|
|
129
|
+
user: session.user,
|
|
130
|
+
};
|
|
131
|
+
await this.saveStore(store);
|
|
132
|
+
this.lastSource = "file";
|
|
133
|
+
this.lastRefreshToken = store.refreshToken;
|
|
134
|
+
return store.accessToken;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Persists a session obtained from login/OAuth completion.
|
|
138
|
+
*/
|
|
139
|
+
async persistSession(session) {
|
|
140
|
+
const store = {
|
|
141
|
+
accessToken: session.accessToken,
|
|
142
|
+
refreshToken: session.refreshToken,
|
|
143
|
+
expiresIn: session.expiresIn,
|
|
144
|
+
fetchedAt: Date.now(),
|
|
145
|
+
user: session.user,
|
|
146
|
+
};
|
|
147
|
+
await this.saveStore(store);
|
|
148
|
+
this.lastSource = "file";
|
|
149
|
+
this.lastRefreshToken = store.refreshToken;
|
|
150
|
+
}
|
|
151
|
+
ensureUnauthenticatedError() {
|
|
152
|
+
throw new OrchynAuthError("Not authenticated with orchyn. Run `npx orchyn-mcp login` to sign in " +
|
|
153
|
+
"with Google, or set the ORCHYN_ACCESS_TOKEN environment variable " +
|
|
154
|
+
"(see `npx orchyn-mcp --help`).");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Builds a TokenProvider for stdio mode (env + file only). */
|
|
158
|
+
export function createStdioTokenProvider(auth) {
|
|
159
|
+
return {
|
|
160
|
+
getAccessToken: async () => auth.getAccessToken(),
|
|
161
|
+
onUnauthorized: async () => auth.onUnauthorized(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Builds a TokenProvider for HTTP mode: env > file > per-session tokens. */
|
|
165
|
+
export function createHttpTokenProvider(auth, session) {
|
|
166
|
+
return {
|
|
167
|
+
getAccessToken: async () => auth.getAccessToken(session),
|
|
168
|
+
onUnauthorized: async () => auth.onUnauthorized(session),
|
|
169
|
+
};
|
|
170
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
export const DEFAULT_BASE_URL = "http://localhost:8080";
|
|
4
|
+
export const DEFAULT_PUBLIC_URL = "http://localhost:3457";
|
|
5
|
+
export const DEFAULT_PORT = 3457;
|
|
6
|
+
export function stripTrailingSlash(url) {
|
|
7
|
+
return url.replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
export function getBaseUrl() {
|
|
10
|
+
const raw = process.env.ORCHYN_BASE_URL || DEFAULT_BASE_URL;
|
|
11
|
+
const url = stripTrailingSlash(raw);
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = new URL(url);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`Invalid ORCHYN_BASE_URL "${raw}": must be a valid http(s) URL.`);
|
|
18
|
+
}
|
|
19
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
20
|
+
throw new Error(`Invalid ORCHYN_BASE_URL "${raw}": only http and https are supported.`);
|
|
21
|
+
}
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
export function getPublicUrl() {
|
|
25
|
+
const raw = process.env.ORCHYN_PUBLIC_URL || DEFAULT_PUBLIC_URL;
|
|
26
|
+
const url = stripTrailingSlash(raw);
|
|
27
|
+
try {
|
|
28
|
+
new URL(url);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error(`Invalid ORCHYN_PUBLIC_URL "${raw}": must be a valid http(s) URL.`);
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
export function getPort() {
|
|
36
|
+
const raw = process.env.ORCHYN_PORT || String(DEFAULT_PORT);
|
|
37
|
+
const port = Number.parseInt(raw, 10);
|
|
38
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error(`Invalid ORCHYN_PORT "${raw}": must be a valid port number.`);
|
|
40
|
+
}
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
export function getCredentialsFile() {
|
|
44
|
+
if (process.env.ORCHYN_CREDENTIALS_FILE) {
|
|
45
|
+
return process.env.ORCHYN_CREDENTIALS_FILE;
|
|
46
|
+
}
|
|
47
|
+
return path.join(os.homedir(), ".config", "orchyn-mcp", "credentials.json");
|
|
48
|
+
}
|
|
49
|
+
export function getTransportMode() {
|
|
50
|
+
const raw = (process.env.ORCHYN_TRANSPORT || "").toLowerCase();
|
|
51
|
+
if (raw === "http")
|
|
52
|
+
return "http";
|
|
53
|
+
return "stdio";
|
|
54
|
+
}
|