@a-venue/guideon-mcp 0.0.0-stage → 0.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/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # @a-venue/guideon-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect Claude, Cursor, Codex and other AI agents to [GuideOn](https://guideon.loretia.com). It is a small
4
+ bridge: your agent talks to it over stdio, and it talks to GuideOn's remote MCP server, signing you in
5
+ through your browser the first time. The agent acts **as you**: it sees your organizations and can change
6
+ what your role allows.
7
+
8
+ ## Use it
9
+
10
+ Claude Code:
11
+
12
+ ```bash
13
+ claude mcp add guideon -- npx -y @a-venue/guideon-mcp
14
+ ```
15
+
16
+ Anything else (Cursor, Claude Desktop, Codex, VS Code), in its MCP settings:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "guideon": { "command": "npx", "args": ["-y", "@a-venue/guideon-mcp"] }
22
+ }
23
+ }
24
+ ```
25
+
26
+ The first run opens your browser; sign in to GuideOn and choose **Allow access**. The token is kept in
27
+ `~/.guideon/mcp-token.json` (readable by you only) and lasts seven days.
28
+
29
+ ## Options
30
+
31
+ | Flag | Environment variable | Default |
32
+ | --- | --- | --- |
33
+ | `--url` | `GUIDEON_MCP_URL` | `https://api.guideon.loretia.com/api/mcp` |
34
+ | `--token` | `GUIDEON_MCP_TOKEN` | the cached sign-in |
35
+
36
+ Logs go to stderr; stdout carries the protocol.
37
+
38
+ ## Tools
39
+
40
+ `list_organizations`, `list_venues`, `get_venue`, `update_venue`, `list_pois`, `create_poi`, `update_poi`,
41
+ `list_avatars`, `list_experiences`, `list_content_items`, `search_knowledge`, `reindex_knowledge`.
package/dist/auth.js ADDED
@@ -0,0 +1,117 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { exec } from 'node:child_process';
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
4
+ import { createServer } from 'node:http';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ export const DEFAULT_URL = 'https://api.guideon.loretia.com/api/mcp';
8
+ const LOGIN_TIMEOUT_MS = 120_000;
9
+ const log = (message) => console.error(`[guideon-mcp] ${message}`);
10
+ const configDir = () => process.env.GUIDEON_MCP_HOME ?? join(homedir(), '.guideon');
11
+ const tokenFile = () => join(configDir(), 'mcp-token.json');
12
+ function readTokens() {
13
+ try {
14
+ return existsSync(tokenFile())
15
+ ? JSON.parse(readFileSync(tokenFile(), 'utf8'))
16
+ : {};
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ /** The cached token for this server, unless it has expired. */
23
+ export function storedToken(url, now = Date.now()) {
24
+ const entry = readTokens()[url];
25
+ return entry && (!entry.expiresAt || entry.expiresAt > now)
26
+ ? entry.accessToken
27
+ : null;
28
+ }
29
+ export function saveToken(url, accessToken, expiresInSeconds) {
30
+ mkdirSync(configDir(), { recursive: true, mode: 0o700 });
31
+ writeFileSync(tokenFile(), JSON.stringify({
32
+ ...readTokens(),
33
+ [url]: {
34
+ accessToken,
35
+ expiresAt: expiresInSeconds ? Date.now() + expiresInSeconds * 1000 : undefined,
36
+ },
37
+ }, null, 2), { mode: 0o600 });
38
+ chmodSync(tokenFile(), 0o600);
39
+ }
40
+ export function openBrowser(url) {
41
+ const command = process.platform === 'darwin'
42
+ ? `open "${url}"`
43
+ : process.platform === 'win32'
44
+ ? `start "" "${url}"`
45
+ : `xdg-open "${url}"`;
46
+ exec(command, (error) => {
47
+ if (error) {
48
+ log(`Open this address in your browser to sign in:\n${url}`);
49
+ }
50
+ });
51
+ }
52
+ const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
53
+ const page = (title, body) => `<!doctype html><meta charset="utf-8"><title>GuideOn</title><body style="font-family:system-ui;text-align:center;padding:64px"><h2>${escapeHtml(title)}</h2><p>${escapeHtml(body)}</p></body>`;
54
+ /**
55
+ * Sign-in by the browser, OAuth authorization-code flow with PKCE: a one-shot server on a
56
+ * random local port catches the redirect, the code is traded for a token at `tokenUrl`.
57
+ * `open` is injectable so a test can play the browser.
58
+ */
59
+ export function signIn(authorizeUrl, tokenUrl, open = openBrowser) {
60
+ const verifier = randomBytes(32).toString('base64url');
61
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
62
+ const state = randomBytes(16).toString('hex');
63
+ return new Promise((resolve, reject) => {
64
+ const server = createServer((req, res) => {
65
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
66
+ if (url.pathname !== '/callback') {
67
+ res.writeHead(404).end();
68
+ return;
69
+ }
70
+ const finish = (status, title, body, outcome) => {
71
+ res.writeHead(status, { 'Content-Type': 'text/html' }).end(page(title, body));
72
+ clearTimeout(timer);
73
+ server.close();
74
+ outcome();
75
+ };
76
+ const code = url.searchParams.get('code');
77
+ const error = url.searchParams.get('error');
78
+ if (error || !code || url.searchParams.get('state') !== state) {
79
+ finish(400, 'Sign-in did not complete', error ?? 'Please try again.', () => reject(new Error(error ? `Sign-in refused: ${error}` : 'Sign-in callback was invalid.')));
80
+ return;
81
+ }
82
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
83
+ fetch(tokenUrl, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({
87
+ grant_type: 'authorization_code',
88
+ code,
89
+ code_verifier: verifier,
90
+ redirect_uri: redirectUri,
91
+ }),
92
+ })
93
+ .then(async (response) => {
94
+ if (!response.ok) {
95
+ throw new Error(`Token exchange failed (${response.status}): ${await response.text()}`);
96
+ }
97
+ const body = (await response.json());
98
+ finish(200, 'You are signed in', 'You can close this tab and go back to your agent.', () => resolve({ accessToken: body.access_token, expiresIn: body.expires_in }));
99
+ })
100
+ .catch((cause) => finish(500, 'Sign-in did not complete', 'Please try again.', () => reject(cause)));
101
+ });
102
+ const timer = setTimeout(() => {
103
+ server.close();
104
+ reject(new Error('Sign-in timed out after two minutes.'));
105
+ }, LOGIN_TIMEOUT_MS);
106
+ server.on('error', reject);
107
+ server.listen(0, '127.0.0.1', () => {
108
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
109
+ const target = new URL(authorizeUrl);
110
+ target.searchParams.set('redirect_uri', redirectUri);
111
+ target.searchParams.set('code_challenge', challenge);
112
+ target.searchParams.set('state', state);
113
+ log('Opening your browser to sign in to GuideOn...');
114
+ open(target.toString());
115
+ });
116
+ });
117
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
7
+ import { DEFAULT_URL, saveToken, signIn, storedToken } from './auth.js';
8
+ // stdout is the protocol; everything a person should read goes to stderr.
9
+ function option(name) {
10
+ const args = process.argv.slice(2);
11
+ const at = args.findIndex((arg) => arg === `--${name}` || arg.startsWith(`--${name}=`));
12
+ if (at === -1) {
13
+ return undefined;
14
+ }
15
+ return args[at].includes('=') ? args[at].split('=').slice(1).join('=') : args[at + 1];
16
+ }
17
+ async function connect(url, token) {
18
+ const client = new Client({ name: 'guideon-mcp', version: '0.1.0' });
19
+ await client.connect(new StreamableHTTPClientTransport(new URL(url), {
20
+ requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : undefined,
21
+ }));
22
+ return client;
23
+ }
24
+ const isUnauthorized = (error) => /\b401\b|unauthori[sz]ed/i.test(error instanceof Error ? error.message : String(error));
25
+ async function login(url) {
26
+ const info = (await (await fetch(url)).json());
27
+ if (!info.authorization_endpoint) {
28
+ throw new Error(`${url} is not a GuideOn MCP server.`);
29
+ }
30
+ const { accessToken, expiresIn } = await signIn(info.authorization_endpoint, `${url.replace(/\/$/, '')}/oauth/token`);
31
+ saveToken(url, accessToken, expiresIn);
32
+ return accessToken;
33
+ }
34
+ async function main() {
35
+ const url = option('url') ?? process.env.GUIDEON_MCP_URL ?? DEFAULT_URL;
36
+ const given = option('token') ?? process.env.GUIDEON_MCP_TOKEN;
37
+ let remote;
38
+ try {
39
+ remote = await connect(url, given ?? storedToken(url));
40
+ }
41
+ catch (error) {
42
+ // A token someone handed us is theirs to fix; ours we can renew.
43
+ if (given || !isUnauthorized(error)) {
44
+ throw error;
45
+ }
46
+ remote = await connect(url, await login(url));
47
+ }
48
+ const local = new Server({ name: 'guideon', version: '0.1.0' }, { capabilities: { tools: {} } });
49
+ local.setRequestHandler(ListToolsRequestSchema, () => remote.listTools());
50
+ local.setRequestHandler(CallToolRequestSchema, (request) => remote.callTool(request.params));
51
+ await local.connect(new StdioServerTransport());
52
+ }
53
+ main().catch((error) => {
54
+ console.error('[guideon-mcp]', error instanceof Error ? error.message : error);
55
+ process.exit(1);
56
+ });
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@a-venue/guideon-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Connect Claude, Cursor, Codex and other AI agents to GuideOn: a stdio bridge to GuideOn's remote MCP server, with browser sign-in.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "guideon",
9
+ "claude",
10
+ "cursor",
11
+ "ai-agent"
12
+ ],
13
+ "license": "UNLICENSED",
14
+ "type": "module",
15
+ "bin": {
16
+ "guideon-mcp": "dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "!dist/*.test.js",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.32.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.10.7",
31
+ "typescript": "5.9.3",
32
+ "@guideon/config": "1.0.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint .",
42
+ "test": "tsc && node --test dist/*.test.js"
43
+ }
6
44
  }