@gpsglobal-ai/gpsglobal 1.4.6 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog — gpsglobal
2
2
 
3
+ ## 1.4.8 — 2026-06-21
4
+
5
+ - **fix(setup):** CLI exits cleanly after `setup` / `login` — clear OAuth timeout, close loopback server, disable axios keep-alive (no more Ctrl+C)
6
+
7
+ ## 1.4.7 — 2026-06-21
8
+
9
+ - **README** — document `get_fund_wiki` `format=linked` default (PDF page deep links)
10
+ - **Docs sync** — `docs/57-mcp/` index, client config, production proof updated to v1.4.6+
11
+
3
12
  ## 1.4.6 — 2026-06-21
4
13
 
5
14
  - **`get_fund_wiki` default `format=linked`** — `{P18}` → markdown link to LP Workspace `?tab=analyze#page=18`
package/README.md CHANGED
@@ -13,7 +13,7 @@ This package implements a [Model Context Protocol (MCP)](https://modelcontextpro
13
13
  | Tool | Description |
14
14
  |------|-------------|
15
15
  | `list_funds` | List funds in your LP vault (with `has_wiki` metadata) |
16
- | `get_fund_wiki` | Return the full markdown wiki for a fund |
16
+ | `get_fund_wiki` | Return fund wiki markdown. **Default `format=linked`** turns `{P18}` citations into clickable PDF page links (`?tab=analyze#page=18`). Use `format=raw` for byte-identical on-disk wiki.md. |
17
17
 
18
18
  Data never leaves GPS infrastructure except through your authenticated session — the MCP proxies to `https://lp.gpsglobal.ai` with LP-scoped JWT or OAuth tokens.
19
19
 
@@ -0,0 +1,5 @@
1
+ import http from 'node:http';
2
+ /** @internal — exported for unit tests only */
3
+ export declare function findFreePort(): Promise<number>;
4
+ /** @internal — exported for unit tests only */
5
+ export declare function closeHttpServer(server: http.Server): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { createServer as createNetServer } from 'node:net';
2
+ /** @internal — exported for unit tests only */
3
+ export async function findFreePort() {
4
+ return new Promise((resolve, reject) => {
5
+ const srv = createNetServer();
6
+ srv.listen(0, '127.0.0.1', () => {
7
+ const addr = srv.address();
8
+ if (!addr || typeof addr === 'string') {
9
+ reject(new Error('Could not bind loopback port'));
10
+ return;
11
+ }
12
+ const port = addr.port;
13
+ srv.close(() => resolve(port));
14
+ });
15
+ srv.on('error', reject);
16
+ });
17
+ }
18
+ /** @internal — exported for unit tests only */
19
+ export function closeHttpServer(server) {
20
+ return new Promise((resolve) => {
21
+ server.closeAllConnections?.();
22
+ server.close(() => resolve());
23
+ });
24
+ }
@@ -1,26 +1,13 @@
1
1
  import http from 'node:http';
2
- import { createServer as createNetServer } from 'node:net';
2
+ import https from 'node:https';
3
3
  import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
4
  import axios from 'axios';
6
5
  import { generatePkce, randomState } from '../lib/pkce.js';
7
6
  import { normalizeApiBase, writeGpsEnvFile, DEFAULT_ENV_PATH } from '../config/env.js';
8
- const execAsync = promisify(exec);
9
- async function findFreePort() {
10
- return new Promise((resolve, reject) => {
11
- const srv = createNetServer();
12
- srv.listen(0, '127.0.0.1', () => {
13
- const addr = srv.address();
14
- if (!addr || typeof addr === 'string') {
15
- reject(new Error('Could not bind loopback port'));
16
- return;
17
- }
18
- const port = addr.port;
19
- srv.close(() => resolve(port));
20
- });
21
- srv.on('error', reject);
22
- });
23
- }
7
+ import { closeHttpServer, findFreePort } from './browser-login-internals.js';
8
+ /** One-shot HTTP clients — avoid keep-alive sockets keeping the CLI alive after setup. */
9
+ const ephemeralHttpAgent = new http.Agent({ keepAlive: false });
10
+ const ephemeralHttpsAgent = new https.Agent({ keepAlive: false });
24
11
  function openBrowser(url) {
25
12
  const cmd = process.platform === 'darwin' ? `open "${url}"` :
26
13
  process.platform === 'win32' ? `start "" "${url}"` :
@@ -38,58 +25,77 @@ export async function browserLogin(apiBase) {
38
25
  authorizeUrl.searchParams.set('state', state);
39
26
  authorizeUrl.searchParams.set('code_challenge', codeChallenge);
40
27
  authorizeUrl.searchParams.set('code_challenge_method', 'S256');
41
- const codePromise = new Promise((resolve, reject) => {
42
- const server = http.createServer((req, res) => {
43
- const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
44
- if (url.pathname !== '/callback') {
45
- res.writeHead(404).end('Not found');
46
- return;
47
- }
48
- const code = url.searchParams.get('code');
49
- const returnedState = url.searchParams.get('state');
50
- if (!code || returnedState !== state) {
51
- res.writeHead(400).end('Invalid callback');
52
- reject(new Error('OAuth callback missing code or state mismatch'));
53
- server.close();
54
- return;
55
- }
56
- res.writeHead(200, { 'Content-Type': 'text/html' });
57
- res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;background:#09090b;color:#fafafa;display:flex;align-items:center;justify-content:center;min-height:100vh">
28
+ let server;
29
+ let timeoutId;
30
+ try {
31
+ const codePromise = new Promise((resolve, reject) => {
32
+ server = http.createServer((req, res) => {
33
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
34
+ if (url.pathname !== '/callback') {
35
+ res.writeHead(404).end('Not found');
36
+ return;
37
+ }
38
+ const code = url.searchParams.get('code');
39
+ const returnedState = url.searchParams.get('state');
40
+ if (!code || returnedState !== state) {
41
+ res.writeHead(400).end('Invalid callback');
42
+ reject(new Error('OAuth callback missing code or state mismatch'));
43
+ return;
44
+ }
45
+ res.writeHead(200, { 'Content-Type': 'text/html' });
46
+ res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;background:#09090b;color:#fafafa;display:flex;align-items:center;justify-content:center;min-height:100vh">
58
47
  <div style="text-align:center"><h1 style="color:#d4af37">GPS MCP connected</h1><p>You can close this tab and return to your terminal.</p></div></body></html>`);
59
- resolve(code);
60
- server.close();
48
+ resolve(code);
49
+ });
50
+ server.listen(port, '127.0.0.1');
51
+ server.on('error', reject);
61
52
  });
62
- server.listen(port, '127.0.0.1');
63
- server.on('error', reject);
64
- });
65
- console.log(`Opening browser for GPS sign-in…\n${authorizeUrl.href}\n`);
66
- openBrowser(authorizeUrl.href);
67
- const code = await Promise.race([
68
- codePromise,
69
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser login timed out after 3 minutes')), 180_000)),
70
- ]);
71
- const tokenResp = await axios.post(`${base}/api/v2/oauth/mcp-cli/token`, {
72
- grant_type: 'authorization_code',
73
- code,
74
- redirect_uri: redirectUri,
75
- code_verifier: codeVerifier,
76
- }, { validateStatus: () => true });
77
- if (tokenResp.status >= 400) {
78
- const desc = tokenResp.data?.error_description ?? tokenResp.statusText;
79
- throw new Error(`Token exchange failed: ${desc}`);
53
+ console.log(`Opening browser for GPS sign-in…\n${authorizeUrl.href}\n`);
54
+ openBrowser(authorizeUrl.href);
55
+ const code = await Promise.race([
56
+ codePromise,
57
+ new Promise((_, reject) => {
58
+ timeoutId = setTimeout(() => reject(new Error('Browser login timed out after 3 minutes')), 180_000);
59
+ }),
60
+ ]);
61
+ if (timeoutId) {
62
+ clearTimeout(timeoutId);
63
+ timeoutId = undefined;
64
+ }
65
+ const tokenResp = await axios.post(`${base}/api/v2/oauth/mcp-cli/token`, {
66
+ grant_type: 'authorization_code',
67
+ code,
68
+ redirect_uri: redirectUri,
69
+ code_verifier: codeVerifier,
70
+ }, {
71
+ validateStatus: () => true,
72
+ httpAgent: ephemeralHttpAgent,
73
+ httpsAgent: ephemeralHttpsAgent,
74
+ });
75
+ if (tokenResp.status >= 400) {
76
+ const desc = tokenResp.data?.error_description ?? tokenResp.statusText;
77
+ throw new Error(`Token exchange failed: ${desc}`);
78
+ }
79
+ const data = tokenResp.data;
80
+ writeGpsEnvFile({
81
+ apiBase: base,
82
+ accessToken: data.access_token,
83
+ lpId: data.lp_id,
84
+ role: data.role,
85
+ username: data.username,
86
+ }, DEFAULT_ENV_PATH);
87
+ return {
88
+ accessToken: data.access_token,
89
+ lpId: data.lp_id,
90
+ username: data.username,
91
+ role: data.role,
92
+ };
93
+ }
94
+ finally {
95
+ if (timeoutId)
96
+ clearTimeout(timeoutId);
97
+ if (server?.listening) {
98
+ await closeHttpServer(server).catch(() => undefined);
99
+ }
80
100
  }
81
- const data = tokenResp.data;
82
- writeGpsEnvFile({
83
- apiBase: base,
84
- accessToken: data.access_token,
85
- lpId: data.lp_id,
86
- role: data.role,
87
- username: data.username,
88
- }, DEFAULT_ENV_PATH);
89
- return {
90
- accessToken: data.access_token,
91
- lpId: data.lp_id,
92
- username: data.username,
93
- role: data.role,
94
- };
95
101
  }
package/dist/cli.js CHANGED
@@ -100,11 +100,11 @@ async function main() {
100
100
  const args = process.argv.slice(2);
101
101
  if (args.includes('--stdio')) {
102
102
  await startStdioServer();
103
- return;
103
+ return true;
104
104
  }
105
105
  if (args.includes('--http')) {
106
106
  await startHttpFromEnv();
107
- return;
107
+ return true;
108
108
  }
109
109
  const cmd = args[0];
110
110
  if (!cmd || cmd === 'help' || args.includes('--help')) {
@@ -118,7 +118,7 @@ async function main() {
118
118
  npx ${PACKAGE} --stdio
119
119
  npx ${PACKAGE} --http
120
120
  `);
121
- return;
121
+ return false;
122
122
  }
123
123
  if (cmd === 'setup') {
124
124
  const hostArg = args.find((a) => a.startsWith('--host='));
@@ -130,31 +130,36 @@ async function main() {
130
130
  useBrowser: !args.includes('--no-browser'),
131
131
  mode: mode ?? 'oauth',
132
132
  });
133
- return;
133
+ return false;
134
134
  }
135
135
  if (cmd === 'doctor') {
136
136
  const result = await runDoctor();
137
137
  printDoctor(result);
138
138
  if (!result.ok)
139
139
  process.exit(1);
140
- return;
140
+ return false;
141
141
  }
142
142
  if (cmd === 'login') {
143
143
  await runLogin(args.includes('--refresh'), args.includes('--browser'));
144
- return;
144
+ return false;
145
145
  }
146
146
  if (cmd === 'status') {
147
147
  runStatus();
148
- return;
148
+ return false;
149
149
  }
150
150
  if (cmd === 'print-config') {
151
151
  printConfig(process.env.GPS_MCP_ENV_PATH ?? DEFAULT_ENV_PATH);
152
- return;
152
+ return false;
153
153
  }
154
154
  console.error(`Unknown command: ${cmd}`);
155
155
  process.exit(1);
156
156
  }
157
- main().catch((err) => {
157
+ main()
158
+ .then((longRunning) => {
159
+ if (!longRunning)
160
+ process.exit(0);
161
+ })
162
+ .catch((err) => {
158
163
  const msg = err instanceof Error ? err.message : String(err);
159
164
  console.error(msg);
160
165
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpsglobal-ai/gpsglobal",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "GPS LP fund wiki MCP server — list_funds and get_fund_wiki for Cursor, Copilot, Claude",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",