brandguideai-mcp 1.0.1 → 1.0.2
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 +16 -2
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +130 -20
- package/dist/index.js +70 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,14 +62,28 @@ Responses include expert answers with source citations from brand strategy books
|
|
|
62
62
|
|
|
63
63
|
Every user gets 4 free queries. No account or API key required — a temporary session is created automatically on first use.
|
|
64
64
|
|
|
65
|
-
After
|
|
65
|
+
After 4 queries, the tool returns upgrade links:
|
|
66
66
|
- **Creator** — 300 queries/month
|
|
67
67
|
- **Pro** — 1,000 queries/month
|
|
68
68
|
- **Mentor+** — unlimited
|
|
69
69
|
|
|
70
70
|
## For existing brandguide/AI subscribers
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Log in once to use your subscription quota:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx brandguideai-mcp login
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Enter your brandguide/AI email and password. A refresh token is stored locally at `~/.brandguide/credentials.json` — your password is never saved.
|
|
79
|
+
|
|
80
|
+
After login, all MCP queries use your subscription quota instead of the 4 free queries.
|
|
81
|
+
|
|
82
|
+
To log out:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx brandguideai-mcp logout
|
|
86
|
+
```
|
|
73
87
|
|
|
74
88
|
## How it works
|
|
75
89
|
|
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearCredentials = clearCredentials;
|
|
4
|
+
exports.loginFlow = loginFlow;
|
|
3
5
|
exports.getAccessToken = getAccessToken;
|
|
4
6
|
exports.getStoredEmail = getStoredEmail;
|
|
5
7
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
6
8
|
const node_fs_1 = require("node:fs");
|
|
7
9
|
const node_path_1 = require("node:path");
|
|
8
10
|
const node_os_1 = require("node:os");
|
|
11
|
+
const node_readline_1 = require("node:readline");
|
|
9
12
|
const constants_js_1 = require("./constants.js");
|
|
10
13
|
const CREDENTIALS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.brandguide');
|
|
11
14
|
const CREDENTIALS_FILE = (0, node_path_1.join)(CREDENTIALS_DIR, 'credentials.json');
|
|
12
15
|
let cachedSession = null;
|
|
16
|
+
function createSupabaseClient() {
|
|
17
|
+
return (0, supabase_js_1.createClient)(constants_js_1.SUPABASE_URL, constants_js_1.SUPABASE_ANON_KEY, {
|
|
18
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
13
21
|
function loadCredentials() {
|
|
14
22
|
try {
|
|
15
23
|
if (!(0, node_fs_1.existsSync)(CREDENTIALS_FILE))
|
|
16
24
|
return null;
|
|
17
25
|
const raw = (0, node_fs_1.readFileSync)(CREDENTIALS_FILE, 'utf-8');
|
|
18
26
|
const data = JSON.parse(raw);
|
|
19
|
-
if (data.email && data.password && data.userId)
|
|
27
|
+
if (data.email && (data.password || data.refreshToken) && data.userId)
|
|
20
28
|
return data;
|
|
21
29
|
return null;
|
|
22
30
|
}
|
|
@@ -35,6 +43,55 @@ function saveCredentials(creds) {
|
|
|
35
43
|
console.error('[auth] Failed to save credentials:', err);
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
function clearCredentials() {
|
|
47
|
+
try {
|
|
48
|
+
if ((0, node_fs_1.existsSync)(CREDENTIALS_FILE)) {
|
|
49
|
+
(0, node_fs_1.unlinkSync)(CREDENTIALS_FILE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
async function prompt(question, hidden = false) {
|
|
55
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
if (hidden) {
|
|
58
|
+
process.stderr.write(question);
|
|
59
|
+
let input = '';
|
|
60
|
+
const stdin = process.stdin;
|
|
61
|
+
const wasRaw = stdin.isRaw;
|
|
62
|
+
if (stdin.isTTY)
|
|
63
|
+
stdin.setRawMode(true);
|
|
64
|
+
stdin.resume();
|
|
65
|
+
const onData = (char) => {
|
|
66
|
+
const c = char.toString();
|
|
67
|
+
if (c === '\n' || c === '\r') {
|
|
68
|
+
if (stdin.isTTY)
|
|
69
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
70
|
+
stdin.removeListener('data', onData);
|
|
71
|
+
process.stderr.write('\n');
|
|
72
|
+
rl.close();
|
|
73
|
+
resolve(input);
|
|
74
|
+
}
|
|
75
|
+
else if (c === '\u0003') {
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
else if (c === '\u007f' || c === '\b') {
|
|
79
|
+
input = input.slice(0, -1);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
input += c;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
stdin.on('data', onData);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
rl.question(question, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
resolve(answer.trim());
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
38
95
|
async function createTempAccount() {
|
|
39
96
|
const response = await fetch(constants_js_1.CREATE_TEMP_SESSION_URL, {
|
|
40
97
|
method: 'POST',
|
|
@@ -60,16 +117,61 @@ async function createTempAccount() {
|
|
|
60
117
|
}
|
|
61
118
|
return { email: data.email, password: data.password, userId: data.userId };
|
|
62
119
|
}
|
|
63
|
-
async function
|
|
64
|
-
const supabase = (
|
|
65
|
-
auth: { persistSession: false, autoRefreshToken: false },
|
|
66
|
-
});
|
|
120
|
+
async function signInWithPassword(email, password) {
|
|
121
|
+
const supabase = createSupabaseClient();
|
|
67
122
|
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
|
68
123
|
if (error || !data.session) {
|
|
69
124
|
throw new Error(error?.message || 'Sign-in failed');
|
|
70
125
|
}
|
|
71
126
|
return data.session;
|
|
72
127
|
}
|
|
128
|
+
async function signInWithRefreshToken(refreshToken) {
|
|
129
|
+
const supabase = createSupabaseClient();
|
|
130
|
+
const { data, error } = await supabase.auth.refreshSession({ refresh_token: refreshToken });
|
|
131
|
+
if (error || !data.session) {
|
|
132
|
+
throw new Error(error?.message || 'Token refresh failed');
|
|
133
|
+
}
|
|
134
|
+
return data.session;
|
|
135
|
+
}
|
|
136
|
+
async function loginFlow() {
|
|
137
|
+
console.error('\nbrandguide/AI MCP — Login\n');
|
|
138
|
+
const email = await prompt('Email: ');
|
|
139
|
+
if (!email) {
|
|
140
|
+
console.error('Email is required.');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const password = await prompt('Password: ', true);
|
|
144
|
+
if (!password) {
|
|
145
|
+
console.error('Password is required.');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const session = await signInWithPassword(email, password);
|
|
150
|
+
const userId = session.user.id;
|
|
151
|
+
// Fetch quota info
|
|
152
|
+
const supabase = createSupabaseClient();
|
|
153
|
+
const { data: quota } = await supabase.rpc('get_remaining_questions', { p_user_id: userId });
|
|
154
|
+
const row = Array.isArray(quota) ? quota[0] : quota;
|
|
155
|
+
const tier = row?.tier || 'freemium';
|
|
156
|
+
const remaining = row?.remaining ?? 0;
|
|
157
|
+
const total = row?.total ?? 0;
|
|
158
|
+
saveCredentials({
|
|
159
|
+
email,
|
|
160
|
+
refreshToken: session.refresh_token,
|
|
161
|
+
userId,
|
|
162
|
+
tier,
|
|
163
|
+
});
|
|
164
|
+
console.error(`\nLogged in as ${email}`);
|
|
165
|
+
console.error(`Tier: ${tier} | Quota: ${remaining}/${total} remaining`);
|
|
166
|
+
console.error(`\nCredentials saved to ~/.brandguide/credentials.json`);
|
|
167
|
+
console.error(`Your subscription quota will be used for MCP queries.\n`);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const message = err instanceof Error ? err.message : 'Login failed';
|
|
171
|
+
console.error(`\nLogin failed: ${message}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
73
175
|
async function getAccessToken() {
|
|
74
176
|
// Check if cached session is still valid (5 min buffer)
|
|
75
177
|
if (cachedSession) {
|
|
@@ -81,22 +183,30 @@ async function getAccessToken() {
|
|
|
81
183
|
}
|
|
82
184
|
// Try stored credentials
|
|
83
185
|
let creds = loadCredentials();
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
186
|
+
if (creds) {
|
|
187
|
+
try {
|
|
188
|
+
// Priority 1: Refresh token (logged-in user)
|
|
189
|
+
if (creds.refreshToken) {
|
|
190
|
+
cachedSession = await signInWithRefreshToken(creds.refreshToken);
|
|
191
|
+
// Update stored refresh token (it rotates)
|
|
192
|
+
saveCredentials({ ...creds, refreshToken: cachedSession.refresh_token });
|
|
193
|
+
return cachedSession.access_token;
|
|
194
|
+
}
|
|
195
|
+
// Priority 2: Email + password (temp account)
|
|
196
|
+
if (creds.password) {
|
|
197
|
+
cachedSession = await signInWithPassword(creds.email, creds.password);
|
|
198
|
+
return cachedSession.access_token;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Credentials stale — fall through to create new temp account
|
|
203
|
+
}
|
|
99
204
|
}
|
|
205
|
+
// Priority 3: Auto-create temp account
|
|
206
|
+
creds = await createTempAccount();
|
|
207
|
+
saveCredentials(creds);
|
|
208
|
+
cachedSession = await signInWithPassword(creds.email, creds.password);
|
|
209
|
+
return cachedSession.access_token;
|
|
100
210
|
}
|
|
101
211
|
function getStoredEmail() {
|
|
102
212
|
const creds = loadCredentials();
|
package/dist/index.js
CHANGED
|
@@ -1,70 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
const auth_js_1 = require("./auth.js");
|
|
5
|
+
// Handle login/logout subcommands before starting MCP server
|
|
6
|
+
const command = process.argv[2];
|
|
7
|
+
if (command === 'login') {
|
|
8
|
+
(0, auth_js_1.loginFlow)().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
9
|
+
}
|
|
10
|
+
else if (command === 'logout') {
|
|
11
|
+
(0, auth_js_1.clearCredentials)();
|
|
12
|
+
console.error('Logged out. Credentials removed from ~/.brandguide/credentials.json');
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// Start MCP server
|
|
17
|
+
startServer();
|
|
18
|
+
}
|
|
19
|
+
async function startServer() {
|
|
20
|
+
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
21
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
22
|
+
const { CallToolRequestSchema, ListToolsRequestSchema, } = await import('@modelcontextprotocol/sdk/types.js');
|
|
23
|
+
const { searchBrandKnowledge } = await import('./client.js');
|
|
24
|
+
const server = new Server({ name: 'brandguideai-mcp', version: '1.0.2' }, { capabilities: { tools: {} } });
|
|
25
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26
|
+
tools: [
|
|
27
|
+
{
|
|
28
|
+
name: 'search_brand_knowledge',
|
|
29
|
+
description: "Search brandguide/AI's brand strategy knowledge base — 1,000+ pages of methodology, design guidelines, and expert video transcripts. Works in Hungarian and English.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
query: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Your brand strategy question',
|
|
36
|
+
},
|
|
20
37
|
},
|
|
38
|
+
required: ['query'],
|
|
21
39
|
},
|
|
22
|
-
required: ['query'],
|
|
23
40
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
],
|
|
42
|
+
}));
|
|
43
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
44
|
+
if (request.params.name !== 'search_brand_knowledge') {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const query = String(request.params.arguments?.query || '').trim();
|
|
51
|
+
if (!query) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: 'Query is required.' }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const result = await searchBrandKnowledge(query);
|
|
59
|
+
if (result.quotaExceeded) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: result.answer }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const footer = result.remaining !== null && result.total !== null
|
|
65
|
+
? `\n\n[${result.remaining}/${result.total} queries remaining]`
|
|
66
|
+
: '';
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: result.answer + footer }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
44
73
|
return {
|
|
45
|
-
content: [{ type: 'text', text:
|
|
74
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
75
|
+
isError: true,
|
|
46
76
|
};
|
|
47
77
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: '';
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: 'text', text: result.answer + footer }],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
57
|
-
return {
|
|
58
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
59
|
-
isError: true,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
async function main() {
|
|
64
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
|
78
|
+
});
|
|
79
|
+
const transport = new StdioServerTransport();
|
|
65
80
|
await server.connect(transport);
|
|
66
81
|
}
|
|
67
|
-
main().catch((err) => {
|
|
68
|
-
console.error('Fatal:', err);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
});
|