@ainote/mcp 1.3.4 → 1.3.6
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 +20 -20
- package/README-ko.md +374 -374
- package/README.md +800 -800
- package/index.js +61 -61
- package/lib/auth/device-flow.js +284 -285
- package/lib/auth/error-message.js +21 -0
- package/lib/auth/mcp-key.js +41 -41
- package/lib/auth/oauth.js +18 -18
- package/lib/auth/refresh.js +85 -78
- package/lib/auth/strategy-factory.js +31 -31
- package/lib/auth/token-store.js +326 -326
- package/lib/cli/call.js +213 -213
- package/lib/cli/login.js +238 -238
- package/lib/cli/logout.js +38 -38
- package/lib/cli/signup.js +130 -118
- package/lib/cli/whoami.js +76 -76
- package/lib/core/api-client.js +1 -1
- package/lib/core/config-manager.js +126 -126
- package/lib/core/config.js +1 -1
- package/lib/core/mcp-server.js +2 -2
- package/lib/core/server-factory.js +82 -82
- package/lib/core/tool-registry.js +31 -31
- package/lib/tools/chatgpt-tools.js +93 -93
- package/lib/tools/dev-doc-tools.js +643 -643
- package/lib/tools/index.js +5 -5
- package/lib/tools/internal/claude-scanner.js +150 -150
- package/lib/tools/internal/encryption.js +88 -88
- package/lib/tools/internal/formatters.js +80 -80
- package/lib/tools/internal/keychain.js +98 -98
- package/lib/tools/internal/path-mapper.js +221 -221
- package/lib/tools/onboarding-tools.js +129 -124
- package/lib/tools/shared-tools.js +280 -280
- package/lib/utils/api-client.js +155 -152
- package/package.json +103 -101
- package/server.js +99 -99
package/lib/auth/mcp-key.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { getConfig } from '../core/config.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Synchronous variant: only consults env var. Kept for callers that cannot
|
|
5
|
-
* await (e.g. the stdio bootstrap path that hasn't loaded the async config).
|
|
6
|
-
*/
|
|
7
|
-
export function authenticateWithMcpKey() {
|
|
8
|
-
const config = getConfig();
|
|
9
|
-
|
|
10
|
-
if (!config.api.key) {
|
|
11
|
-
throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
type: 'mcpKey',
|
|
16
|
-
apiKey: config.api.key
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Async variant: falls back to the local CLI token store (populated by
|
|
22
|
-
* `ainote-mcp login`) when no env var is set.
|
|
23
|
-
*/
|
|
24
|
-
export async function authenticateWithMcpKeyAsync() {
|
|
25
|
-
const envKey = process.env.AINOTE_API_KEY;
|
|
26
|
-
if (envKey) {
|
|
27
|
-
return { type: 'mcpKey', apiKey: envKey };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const { getCredentials } = await import('./token-store.js');
|
|
32
|
-
const { resolveApiUrl } = await import('../core/config-manager.js');
|
|
33
|
-
const baseUrl = resolveApiUrl();
|
|
34
|
-
const creds = await getCredentials(baseUrl);
|
|
35
|
-
if (creds && creds.mcpKey) {
|
|
36
|
-
return { type: 'mcpKey', apiKey: creds.mcpKey };
|
|
37
|
-
}
|
|
38
|
-
} catch { /* fall through */ }
|
|
39
|
-
|
|
40
|
-
throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
|
|
41
|
-
}
|
|
1
|
+
import { getConfig } from '../core/config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous variant: only consults env var. Kept for callers that cannot
|
|
5
|
+
* await (e.g. the stdio bootstrap path that hasn't loaded the async config).
|
|
6
|
+
*/
|
|
7
|
+
export function authenticateWithMcpKey() {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
|
|
10
|
+
if (!config.api.key) {
|
|
11
|
+
throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'mcpKey',
|
|
16
|
+
apiKey: config.api.key
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Async variant: falls back to the local CLI token store (populated by
|
|
22
|
+
* `ainote-mcp login`) when no env var is set.
|
|
23
|
+
*/
|
|
24
|
+
export async function authenticateWithMcpKeyAsync() {
|
|
25
|
+
const envKey = process.env.AINOTE_API_KEY;
|
|
26
|
+
if (envKey) {
|
|
27
|
+
return { type: 'mcpKey', apiKey: envKey };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const { getCredentials } = await import('./token-store.js');
|
|
32
|
+
const { resolveApiUrl } = await import('../core/config-manager.js');
|
|
33
|
+
const baseUrl = resolveApiUrl();
|
|
34
|
+
const creds = await getCredentials(baseUrl);
|
|
35
|
+
if (creds && creds.mcpKey) {
|
|
36
|
+
return { type: 'mcpKey', apiKey: creds.mcpKey };
|
|
37
|
+
}
|
|
38
|
+
} catch { /* fall through */ }
|
|
39
|
+
|
|
40
|
+
throw new Error('AINOTE_API_KEY is required to authenticate MCP requests. Run `ainote-mcp login` or set AINOTE_API_KEY.');
|
|
41
|
+
}
|
package/lib/auth/oauth.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export function authenticateWithOAuth(headers = {}) {
|
|
2
|
-
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
3
|
-
|
|
4
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
5
|
-
throw new Error('Missing or invalid Bearer token for OAuth authentication');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const token = authHeader.substring('Bearer '.length).trim();
|
|
9
|
-
|
|
10
|
-
if (!token) {
|
|
11
|
-
throw new Error('Bearer token is empty');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
type: 'oauth',
|
|
16
|
-
token
|
|
17
|
-
};
|
|
18
|
-
}
|
|
1
|
+
export function authenticateWithOAuth(headers = {}) {
|
|
2
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
3
|
+
|
|
4
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
5
|
+
throw new Error('Missing or invalid Bearer token for OAuth authentication');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const token = authHeader.substring('Bearer '.length).trim();
|
|
9
|
+
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new Error('Bearer token is empty');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'oauth',
|
|
16
|
+
token
|
|
17
|
+
};
|
|
18
|
+
}
|
package/lib/auth/refresh.js
CHANGED
|
@@ -1,78 +1,85 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access token rotation against the ainote backend.
|
|
3
|
-
*
|
|
4
|
-
* Endpoint: POST /api/auth/token/refresh
|
|
5
|
-
* Body: { refresh_token: "rt_..." }
|
|
6
|
-
* Response: { access_token, refresh_token, expires_in }
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Access token rotation against the ainote backend.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: POST /api/auth/token/refresh
|
|
5
|
+
* Body: { refresh_token: "rt_..." }
|
|
6
|
+
* Response: { success, data: { access_token, refresh_token, expires_in } }
|
|
7
|
+
* (legacy: same fields at the top level)
|
|
8
|
+
*
|
|
9
|
+
* Concurrency: an in-process mutex (module-level Promise) ensures that
|
|
10
|
+
* parallel callers wait on a single in-flight refresh instead of issuing
|
|
11
|
+
* duplicate requests (which would otherwise burn through refresh-token
|
|
12
|
+
* rotation and invalidate the family).
|
|
13
|
+
*/
|
|
14
|
+
import axios from 'axios';
|
|
15
|
+
import { errorMessage } from './error-message.js';
|
|
16
|
+
|
|
17
|
+
let inFlight = null;
|
|
18
|
+
|
|
19
|
+
export class RefreshError extends Error {
|
|
20
|
+
constructor(message, { status, body } = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'RefreshError';
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.body = body;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function performRefresh({ apiUrl, refreshToken }) {
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await axios.post(
|
|
32
|
+
`${apiUrl}/api/auth/token/refresh`,
|
|
33
|
+
{ refresh_token: refreshToken },
|
|
34
|
+
{
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
validateStatus: (s) => s >= 200 && s < 500
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new RefreshError(`Refresh request failed: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (response.status < 200 || response.status >= 300) {
|
|
45
|
+
const body = response.data || {};
|
|
46
|
+
const msg = errorMessage(body) ||
|
|
47
|
+
`Refresh failed with HTTP ${response.status}`;
|
|
48
|
+
throw new RefreshError(msg, { status: response.status, body });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const body = response.data || {};
|
|
52
|
+
// Server wraps the payload as { success, data: { access_token, ... } };
|
|
53
|
+
// older servers returned the fields at the top level.
|
|
54
|
+
const accessToken = body.data?.access_token ?? body.access_token;
|
|
55
|
+
const nextRefreshToken = body.data?.refresh_token ?? body.refresh_token;
|
|
56
|
+
const expiresIn = body.data?.expires_in ?? body.expires_in;
|
|
57
|
+
if (!accessToken) {
|
|
58
|
+
throw new RefreshError('Refresh response missing access_token', { body });
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
access_token: accessToken,
|
|
62
|
+
refresh_token: nextRefreshToken || refreshToken,
|
|
63
|
+
expires_in: Number(expiresIn) || undefined
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Rotate the access token. Concurrent callers share one in-flight request.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} args
|
|
71
|
+
* @param {string} args.apiUrl
|
|
72
|
+
* @param {string} args.refreshToken
|
|
73
|
+
* @returns {Promise<{access_token: string, refresh_token: string, expires_in?: number}>}
|
|
74
|
+
*/
|
|
75
|
+
export async function refreshAccessToken({ apiUrl, refreshToken }) {
|
|
76
|
+
if (!refreshToken) {
|
|
77
|
+
throw new RefreshError('No refresh token available — login required.');
|
|
78
|
+
}
|
|
79
|
+
if (inFlight) return inFlight;
|
|
80
|
+
inFlight = performRefresh({ apiUrl, refreshToken })
|
|
81
|
+
.finally(() => {
|
|
82
|
+
inFlight = null;
|
|
83
|
+
});
|
|
84
|
+
return inFlight;
|
|
85
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { authenticateWithMcpKey } from './mcp-key.js';
|
|
2
|
-
import { authenticateWithOAuth } from './oauth.js';
|
|
3
|
-
import { getFeatureFlags } from '../core/config.js';
|
|
4
|
-
|
|
5
|
-
export function resolveAuthContext(transportType, headers = {}) {
|
|
6
|
-
if (transportType === 'stdio') {
|
|
7
|
-
try {
|
|
8
|
-
return authenticateWithMcpKey();
|
|
9
|
-
} catch {
|
|
10
|
-
// No API key configured — allow anonymous mode for onboarding tools
|
|
11
|
-
return { type: 'anonymous' };
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const features = getFeatureFlags();
|
|
16
|
-
|
|
17
|
-
if (transportType === 'sse' && features.oauthAuth) {
|
|
18
|
-
try {
|
|
19
|
-
return authenticateWithOAuth(headers);
|
|
20
|
-
} catch {
|
|
21
|
-
// Fall through to MCP key or anonymous
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Default fallback to MCP key, then anonymous for onboarding
|
|
26
|
-
try {
|
|
27
|
-
return authenticateWithMcpKey();
|
|
28
|
-
} catch {
|
|
29
|
-
return { type: 'anonymous' };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
import { authenticateWithMcpKey } from './mcp-key.js';
|
|
2
|
+
import { authenticateWithOAuth } from './oauth.js';
|
|
3
|
+
import { getFeatureFlags } from '../core/config.js';
|
|
4
|
+
|
|
5
|
+
export function resolveAuthContext(transportType, headers = {}) {
|
|
6
|
+
if (transportType === 'stdio') {
|
|
7
|
+
try {
|
|
8
|
+
return authenticateWithMcpKey();
|
|
9
|
+
} catch {
|
|
10
|
+
// No API key configured — allow anonymous mode for onboarding tools
|
|
11
|
+
return { type: 'anonymous' };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const features = getFeatureFlags();
|
|
16
|
+
|
|
17
|
+
if (transportType === 'sse' && features.oauthAuth) {
|
|
18
|
+
try {
|
|
19
|
+
return authenticateWithOAuth(headers);
|
|
20
|
+
} catch {
|
|
21
|
+
// Fall through to MCP key or anonymous
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Default fallback to MCP key, then anonymous for onboarding
|
|
26
|
+
try {
|
|
27
|
+
return authenticateWithMcpKey();
|
|
28
|
+
} catch {
|
|
29
|
+
return { type: 'anonymous' };
|
|
30
|
+
}
|
|
31
|
+
}
|