@ainote/mcp 1.3.2 → 1.3.3
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 +285 -274
- package/lib/auth/mcp-key.js +40 -40
- package/lib/auth/oauth.js +18 -18
- package/lib/auth/refresh.js +78 -78
- package/lib/auth/strategy-factory.js +31 -31
- package/lib/auth/token-store.js +302 -286
- package/lib/cli/call.js +211 -211
- package/lib/cli/login.js +238 -238
- package/lib/cli/logout.js +39 -39
- package/lib/cli/signup.js +118 -118
- package/lib/cli/whoami.js +77 -77
- package/lib/core/api-client.js +1 -1
- package/lib/core/config-manager.js +106 -106
- 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 +124 -124
- package/lib/tools/shared-tools.js +280 -280
- package/lib/utils/api-client.js +152 -152
- package/package.json +101 -101
- package/server.js +99 -99
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,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: { access_token, refresh_token, expires_in }
|
|
7
|
-
*
|
|
8
|
-
* Concurrency: an in-process mutex (module-level Promise) ensures that
|
|
9
|
-
* parallel callers wait on a single in-flight refresh instead of issuing
|
|
10
|
-
* duplicate requests (which would otherwise burn through refresh-token
|
|
11
|
-
* rotation and invalidate the family).
|
|
12
|
-
*/
|
|
13
|
-
import axios from 'axios';
|
|
14
|
-
|
|
15
|
-
let inFlight = null;
|
|
16
|
-
|
|
17
|
-
export class RefreshError extends Error {
|
|
18
|
-
constructor(message, { status, body } = {}) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = 'RefreshError';
|
|
21
|
-
this.status = status;
|
|
22
|
-
this.body = body;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function performRefresh({ apiUrl, refreshToken }) {
|
|
27
|
-
let response;
|
|
28
|
-
try {
|
|
29
|
-
response = await axios.post(
|
|
30
|
-
`${apiUrl}/api/auth/token/refresh`,
|
|
31
|
-
{ refresh_token: refreshToken },
|
|
32
|
-
{
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
timeout: 30000,
|
|
35
|
-
validateStatus: (s) => s >= 200 && s < 500
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
throw new RefreshError(`Refresh request failed: ${err.message}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (response.status < 200 || response.status >= 300) {
|
|
43
|
-
const body = response.data || {};
|
|
44
|
-
const msg = body.error_description || body.error || body.message ||
|
|
45
|
-
`Refresh failed with HTTP ${response.status}`;
|
|
46
|
-
throw new RefreshError(msg, { status: response.status, body });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const body = response.data || {};
|
|
50
|
-
if (!body.access_token) {
|
|
51
|
-
throw new RefreshError('Refresh response missing access_token', { body });
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
access_token: body.access_token,
|
|
55
|
-
refresh_token: body.refresh_token || refreshToken,
|
|
56
|
-
expires_in: Number(body.expires_in) || undefined
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Rotate the access token. Concurrent callers share one in-flight request.
|
|
62
|
-
*
|
|
63
|
-
* @param {Object} args
|
|
64
|
-
* @param {string} args.apiUrl
|
|
65
|
-
* @param {string} args.refreshToken
|
|
66
|
-
* @returns {Promise<{access_token: string, refresh_token: string, expires_in?: number}>}
|
|
67
|
-
*/
|
|
68
|
-
export async function refreshAccessToken({ apiUrl, refreshToken }) {
|
|
69
|
-
if (!refreshToken) {
|
|
70
|
-
throw new RefreshError('No refresh token available — login required.');
|
|
71
|
-
}
|
|
72
|
-
if (inFlight) return inFlight;
|
|
73
|
-
inFlight = performRefresh({ apiUrl, refreshToken })
|
|
74
|
-
.finally(() => {
|
|
75
|
-
inFlight = null;
|
|
76
|
-
});
|
|
77
|
-
return inFlight;
|
|
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: { access_token, refresh_token, expires_in }
|
|
7
|
+
*
|
|
8
|
+
* Concurrency: an in-process mutex (module-level Promise) ensures that
|
|
9
|
+
* parallel callers wait on a single in-flight refresh instead of issuing
|
|
10
|
+
* duplicate requests (which would otherwise burn through refresh-token
|
|
11
|
+
* rotation and invalidate the family).
|
|
12
|
+
*/
|
|
13
|
+
import axios from 'axios';
|
|
14
|
+
|
|
15
|
+
let inFlight = null;
|
|
16
|
+
|
|
17
|
+
export class RefreshError extends Error {
|
|
18
|
+
constructor(message, { status, body } = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'RefreshError';
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.body = body;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function performRefresh({ apiUrl, refreshToken }) {
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await axios.post(
|
|
30
|
+
`${apiUrl}/api/auth/token/refresh`,
|
|
31
|
+
{ refresh_token: refreshToken },
|
|
32
|
+
{
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
validateStatus: (s) => s >= 200 && s < 500
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new RefreshError(`Refresh request failed: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (response.status < 200 || response.status >= 300) {
|
|
43
|
+
const body = response.data || {};
|
|
44
|
+
const msg = body.error_description || body.error || body.message ||
|
|
45
|
+
`Refresh failed with HTTP ${response.status}`;
|
|
46
|
+
throw new RefreshError(msg, { status: response.status, body });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = response.data || {};
|
|
50
|
+
if (!body.access_token) {
|
|
51
|
+
throw new RefreshError('Refresh response missing access_token', { body });
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
access_token: body.access_token,
|
|
55
|
+
refresh_token: body.refresh_token || refreshToken,
|
|
56
|
+
expires_in: Number(body.expires_in) || undefined
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Rotate the access token. Concurrent callers share one in-flight request.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} args
|
|
64
|
+
* @param {string} args.apiUrl
|
|
65
|
+
* @param {string} args.refreshToken
|
|
66
|
+
* @returns {Promise<{access_token: string, refresh_token: string, expires_in?: number}>}
|
|
67
|
+
*/
|
|
68
|
+
export async function refreshAccessToken({ apiUrl, refreshToken }) {
|
|
69
|
+
if (!refreshToken) {
|
|
70
|
+
throw new RefreshError('No refresh token available — login required.');
|
|
71
|
+
}
|
|
72
|
+
if (inFlight) return inFlight;
|
|
73
|
+
inFlight = performRefresh({ apiUrl, refreshToken })
|
|
74
|
+
.finally(() => {
|
|
75
|
+
inFlight = null;
|
|
76
|
+
});
|
|
77
|
+
return inFlight;
|
|
78
|
+
}
|
|
@@ -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
|
+
}
|