@meldocio/mcp-stdio-proxy 1.0.7 → 1.0.10

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/lib/auth.js ADDED
@@ -0,0 +1,142 @@
1
+ const axios = require('axios');
2
+ const https = require('https');
3
+ const { readCredentials, writeCredentials, isTokenExpiredOrExpiringSoon, isTokenExpired } = require('./credentials');
4
+ const { getApiUrl } = require('./constants');
5
+
6
+ /**
7
+ * Get access token with priority:
8
+ * 1. MELDOC_ACCESS_TOKEN (environment variable)
9
+ * 2. credentials.json (user session, with refresh if needed)
10
+ * 3. MELDOC_MCP_TOKEN (integration token)
11
+ * @returns {Promise<{token: string, type: 'env'|'user_session'|'integration'|null}>}
12
+ */
13
+ async function getAccessToken() {
14
+ // Priority 1: MELDOC_ACCESS_TOKEN environment variable
15
+ if (process.env.MELDOC_ACCESS_TOKEN) {
16
+ return {
17
+ token: process.env.MELDOC_ACCESS_TOKEN,
18
+ type: 'env'
19
+ };
20
+ }
21
+
22
+ // Priority 2: credentials.json (user session)
23
+ const credentials = readCredentials();
24
+ if (credentials && credentials.type === 'user_session') {
25
+ // Check if token needs refresh
26
+ if (isTokenExpiredOrExpiringSoon(credentials)) {
27
+ try {
28
+ const refreshed = await refreshToken(credentials);
29
+ if (refreshed) {
30
+ return {
31
+ token: refreshed.tokens.accessToken,
32
+ type: 'user_session'
33
+ };
34
+ }
35
+ } catch (error) {
36
+ // Refresh failed - credentials might be invalid
37
+ // Return null to fall back to next priority
38
+ }
39
+ } else {
40
+ return {
41
+ token: credentials.tokens.accessToken,
42
+ type: 'user_session'
43
+ };
44
+ }
45
+ }
46
+
47
+ // Priority 3: MELDOC_MCP_TOKEN (integration token)
48
+ if (process.env.MELDOC_MCP_TOKEN) {
49
+ return {
50
+ token: process.env.MELDOC_MCP_TOKEN,
51
+ type: 'integration'
52
+ };
53
+ }
54
+
55
+ // No token found
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Refresh access token using refresh token
61
+ * @param {Object} credentials - Current credentials object
62
+ * @returns {Promise<Object|null>} Updated credentials or null if refresh failed
63
+ */
64
+ async function refreshToken(credentials) {
65
+ if (!credentials || !credentials.tokens || !credentials.tokens.refreshToken) {
66
+ return null;
67
+ }
68
+
69
+ const apiBaseUrl = credentials.apiBaseUrl || getApiUrl();
70
+
71
+ try {
72
+ const response = await axios.post(`${apiBaseUrl}/api/auth/refresh`, {
73
+ refreshToken: credentials.tokens.refreshToken
74
+ }, {
75
+ timeout: 10000,
76
+ httpsAgent: new https.Agent({ keepAlive: true })
77
+ });
78
+
79
+ // Update credentials with new tokens
80
+ const updatedCredentials = {
81
+ ...credentials,
82
+ tokens: {
83
+ accessToken: response.data.accessToken,
84
+ accessExpiresAt: response.data.expiresAt,
85
+ refreshToken: response.data.refreshToken || credentials.tokens.refreshToken
86
+ },
87
+ updatedAt: new Date().toISOString()
88
+ };
89
+
90
+ writeCredentials(updatedCredentials);
91
+ return updatedCredentials;
92
+ } catch (error) {
93
+ // Refresh failed - delete credentials
94
+ const { deleteCredentials } = require('./credentials');
95
+ deleteCredentials();
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if user is authenticated
102
+ * @returns {Promise<boolean>} True if authenticated
103
+ */
104
+ async function isAuthenticated() {
105
+ const tokenInfo = await getAccessToken();
106
+ return tokenInfo !== null;
107
+ }
108
+
109
+ /**
110
+ * Get auth status information
111
+ * @returns {Promise<Object|null>} Auth status with user info or null if not authenticated
112
+ */
113
+ async function getAuthStatus() {
114
+ const tokenInfo = await getAccessToken();
115
+ if (!tokenInfo) {
116
+ return null;
117
+ }
118
+
119
+ if (tokenInfo.type === 'user_session') {
120
+ const credentials = readCredentials();
121
+ if (credentials && credentials.user) {
122
+ return {
123
+ authenticated: true,
124
+ type: 'user_session',
125
+ user: credentials.user,
126
+ expiresAt: credentials.tokens?.accessExpiresAt || null
127
+ };
128
+ }
129
+ }
130
+
131
+ return {
132
+ authenticated: true,
133
+ type: tokenInfo.type
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ getAccessToken,
139
+ refreshToken,
140
+ isAuthenticated,
141
+ getAuthStatus
142
+ };
package/lib/config.js ADDED
@@ -0,0 +1,68 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_PATH = path.join(os.homedir(), '.meldoc', 'config.json');
6
+
7
+ /**
8
+ * Read config from ~/.meldoc/config.json
9
+ * @returns {Object|null} Config object or null if file doesn't exist
10
+ */
11
+ function readConfig() {
12
+ try {
13
+ if (!fs.existsSync(CONFIG_PATH)) {
14
+ return null;
15
+ }
16
+ const content = fs.readFileSync(CONFIG_PATH, 'utf8');
17
+ return JSON.parse(content);
18
+ } catch (error) {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Write config to ~/.meldoc/config.json
25
+ * Sets file permissions to 0644 (read for all, write for owner)
26
+ * @param {Object} config - Config object
27
+ */
28
+ function writeConfig(config) {
29
+ const dir = path.dirname(CONFIG_PATH);
30
+
31
+ // Create .meldoc directory if it doesn't exist
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ }
35
+
36
+ // Write config file
37
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', {
38
+ encoding: 'utf8',
39
+ mode: 0o644 // 0644 - read for all, write for owner
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Get workspace alias from config
45
+ * @returns {string|null} Workspace alias or null
46
+ */
47
+ function getWorkspaceAlias() {
48
+ const config = readConfig();
49
+ return config?.workspaceAlias || null;
50
+ }
51
+
52
+ /**
53
+ * Set workspace alias in config
54
+ * @param {string} alias - Workspace alias
55
+ */
56
+ function setWorkspaceAlias(alias) {
57
+ const config = readConfig() || {};
58
+ config.workspaceAlias = alias;
59
+ writeConfig(config);
60
+ }
61
+
62
+ module.exports = {
63
+ readConfig,
64
+ writeConfig,
65
+ getWorkspaceAlias,
66
+ setWorkspaceAlias,
67
+ CONFIG_PATH
68
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Default configuration constants
3
+ * Production values used when environment variables are not set
4
+ */
5
+
6
+ // Production API URL
7
+ const DEFAULT_API_URL = 'https://api.meldoc.io';
8
+
9
+ // Production App URL (frontend)
10
+ const DEFAULT_APP_URL = 'https://app.meldoc.io';
11
+
12
+ /**
13
+ * Get API URL from environment or use production default
14
+ */
15
+ function getApiUrl() {
16
+ return process.env.MELDOC_API_URL || DEFAULT_API_URL;
17
+ }
18
+
19
+ /**
20
+ * Get App URL from environment or use production default
21
+ */
22
+ function getAppUrl() {
23
+ return process.env.MELDOC_APP_URL || DEFAULT_APP_URL;
24
+ }
25
+
26
+ module.exports = {
27
+ DEFAULT_API_URL,
28
+ DEFAULT_APP_URL,
29
+ getApiUrl,
30
+ getAppUrl
31
+ };
@@ -0,0 +1,96 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CREDENTIALS_PATH = path.join(os.homedir(), '.meldoc', 'credentials.json');
6
+
7
+ /**
8
+ * Read credentials from ~/.meldoc/credentials.json
9
+ * @returns {Object|null} Credentials object or null if file doesn't exist
10
+ */
11
+ function readCredentials() {
12
+ try {
13
+ if (!fs.existsSync(CREDENTIALS_PATH)) {
14
+ return null;
15
+ }
16
+ const content = fs.readFileSync(CREDENTIALS_PATH, 'utf8');
17
+ return JSON.parse(content);
18
+ } catch (error) {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Write credentials to ~/.meldoc/credentials.json
25
+ * Sets file permissions to 0600 (read/write only for owner)
26
+ * @param {Object} credentials - Credentials object
27
+ */
28
+ function writeCredentials(credentials) {
29
+ const dir = path.dirname(CREDENTIALS_PATH);
30
+
31
+ // Create .meldoc directory if it doesn't exist
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ }
35
+
36
+ // Write credentials file
37
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2) + '\n', {
38
+ encoding: 'utf8',
39
+ mode: 0o600 // 0600 - read/write only for owner
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Delete credentials file
45
+ */
46
+ function deleteCredentials() {
47
+ try {
48
+ if (fs.existsSync(CREDENTIALS_PATH)) {
49
+ fs.unlinkSync(CREDENTIALS_PATH);
50
+ }
51
+ } catch (error) {
52
+ // Ignore errors
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Check if access token is expired or expires soon (within 5 minutes)
58
+ * @param {Object} credentials - Credentials object
59
+ * @returns {boolean} True if token is expired or expires soon
60
+ */
61
+ function isTokenExpiredOrExpiringSoon(credentials) {
62
+ if (!credentials || !credentials.tokens || !credentials.tokens.accessExpiresAt) {
63
+ return true;
64
+ }
65
+
66
+ const expiresAt = new Date(credentials.tokens.accessExpiresAt);
67
+ const now = new Date();
68
+ const fiveMinutes = 5 * 60 * 1000; // 5 minutes in milliseconds
69
+
70
+ return expiresAt.getTime() <= (now.getTime() + fiveMinutes);
71
+ }
72
+
73
+ /**
74
+ * Check if access token is expired
75
+ * @param {Object} credentials - Credentials object
76
+ * @returns {boolean} True if token is expired
77
+ */
78
+ function isTokenExpired(credentials) {
79
+ if (!credentials || !credentials.tokens || !credentials.tokens.accessExpiresAt) {
80
+ return true;
81
+ }
82
+
83
+ const expiresAt = new Date(credentials.tokens.accessExpiresAt);
84
+ const now = new Date();
85
+
86
+ return expiresAt.getTime() <= now.getTime();
87
+ }
88
+
89
+ module.exports = {
90
+ readCredentials,
91
+ writeCredentials,
92
+ deleteCredentials,
93
+ isTokenExpiredOrExpiringSoon,
94
+ isTokenExpired,
95
+ CREDENTIALS_PATH
96
+ };
@@ -0,0 +1,263 @@
1
+ const axios = require('axios');
2
+ const https = require('https');
3
+ const { writeCredentials } = require('./credentials');
4
+ const { getApiUrl, getAppUrl } = require('./constants');
5
+ const logger = require('./logger');
6
+
7
+ /**
8
+ * Start device flow authentication
9
+ * @param {string} apiBaseUrl - API base URL
10
+ * @returns {Promise<Object>} Device flow response with deviceCode, userCode, verificationUrl, expiresIn, interval
11
+ */
12
+ async function startDeviceFlow(apiBaseUrl = null) {
13
+ const url = apiBaseUrl || getApiUrl();
14
+ try {
15
+ // Server may expect snake_case, but we'll send camelCase and let server handle it
16
+ const response = await axios.post(`${url}/api/auth/device/start`, {
17
+ client: 'mcp-stdio-proxy',
18
+ client_version: '1.0.0'
19
+ }, {
20
+ timeout: 10000,
21
+ httpsAgent: new https.Agent({ keepAlive: true })
22
+ });
23
+
24
+ const data = response.data;
25
+
26
+ // Normalize response: support both camelCase and snake_case
27
+ const normalized = {
28
+ deviceCode: data.deviceCode || data.device_code,
29
+ userCode: data.userCode || data.user_code,
30
+ verificationUrl: data.verificationUrl || data.verification_url,
31
+ expiresIn: data.expiresIn || data.expires_in,
32
+ interval: data.interval
33
+ };
34
+
35
+ // Validate response
36
+ if (!normalized.deviceCode || !normalized.userCode || !normalized.verificationUrl) {
37
+ throw new Error(`Invalid response from server: missing required fields. Response: ${JSON.stringify(data)}`);
38
+ }
39
+
40
+ if (!normalized.expiresIn || !normalized.interval) {
41
+ throw new Error(`Invalid response from server: missing expiresIn or interval. Response: ${JSON.stringify(data)}`);
42
+ }
43
+
44
+ return normalized;
45
+ } catch (error) {
46
+ if (error.response) {
47
+ // Server responded with error status
48
+ const status = error.response.status;
49
+ const errorData = error.response.data;
50
+ const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${status}: ${error.response.statusText}`;
51
+ throw new Error(`Device flow start failed: ${errorMessage}`);
52
+ } else if (error.request) {
53
+ // Request was made but no response received
54
+ throw new Error(`Device flow start failed: No response from server at ${url}`);
55
+ } else {
56
+ // Other errors
57
+ throw new Error(`Device flow start failed: ${error.message}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Poll device flow for approval
64
+ * @param {string} deviceCode - Device code from startDeviceFlow
65
+ * @param {string} apiBaseUrl - API base URL
66
+ * @returns {Promise<Object>} Poll response with status and tokens if approved
67
+ */
68
+ async function pollDeviceFlow(deviceCode, apiBaseUrl = null) {
69
+ const url = apiBaseUrl || getApiUrl();
70
+ try {
71
+ // Server expects snake_case: device_code
72
+ const response = await axios.post(`${url}/api/auth/device/poll`, {
73
+ device_code: deviceCode
74
+ }, {
75
+ timeout: 10000,
76
+ httpsAgent: new https.Agent({ keepAlive: true })
77
+ });
78
+
79
+ const data = response.data;
80
+
81
+ // Debug logging - only log if LOG_LEVEL is DEBUG
82
+ if (process.env.LOG_LEVEL === 'DEBUG') {
83
+ logger.debug(`Poll response keys: ${Object.keys(data).join(', ')}`);
84
+ logger.debug(`Poll response: ${JSON.stringify(data, null, 2)}`);
85
+ }
86
+
87
+ // Validate response has status field
88
+ if (!data.status) {
89
+ throw new Error(`Invalid poll response: missing status field. Response: ${JSON.stringify(data)}`);
90
+ }
91
+
92
+ // Check if data is nested in auth_response (server format)
93
+ const authData = data.auth_response || data.authResponse || data;
94
+
95
+ // refresh_token can be in root or in auth_response
96
+ const refreshToken = data.refresh_token || data.refreshToken ||
97
+ authData.refreshToken || authData.refresh_token || null;
98
+
99
+ // Normalize response: support both camelCase and snake_case
100
+ // Check all possible field name variations
101
+ const normalized = {
102
+ status: data.status,
103
+ accessToken: authData.accessToken || authData.access_token || null,
104
+ expiresAt: authData.expiresAt || authData.expires_at || null,
105
+ refreshToken: refreshToken,
106
+ user: authData.user || null
107
+ };
108
+
109
+ // Debug logging - only log if LOG_LEVEL is DEBUG
110
+ if (process.env.LOG_LEVEL === 'DEBUG') {
111
+ logger.debug(`Normalized response: ${JSON.stringify({
112
+ ...normalized,
113
+ accessToken: normalized.accessToken ? '[REDACTED]' : null
114
+ }, null, 2)}`);
115
+ }
116
+
117
+ // Validate normalized response for approved status
118
+ if (normalized.status === 'approved' && !normalized.accessToken) {
119
+ throw new Error(`Invalid approved response: missing accessToken. Raw response: ${JSON.stringify(data)}, Normalized: ${JSON.stringify(normalized)}`);
120
+ }
121
+
122
+ return normalized;
123
+ } catch (error) {
124
+ if (error.response) {
125
+ // Server responded with error status
126
+ const status = error.response.status;
127
+ const errorData = error.response.data;
128
+ const errorMessage = errorData?.error?.message || errorData?.message || `HTTP ${status}: ${error.response.statusText}`;
129
+ throw new Error(`Device flow poll failed: ${errorMessage}`);
130
+ } else if (error.request) {
131
+ // Request was made but no response received
132
+ throw new Error(`Device flow poll failed: No response from server at ${url}`);
133
+ } else {
134
+ // Other errors (including our validation errors)
135
+ throw error;
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Perform device flow login
142
+ * @param {Function} onCodeDisplay - Callback to display code to user (url, code)
143
+ * @param {Function} onStatusChange - Optional callback for status changes
144
+ * @param {string} apiBaseUrl - API base URL
145
+ * @returns {Promise<Object>} Credentials object
146
+ */
147
+ async function deviceFlowLogin(onCodeDisplay, onStatusChange = null, apiBaseUrl = null, appUrl = null) {
148
+ const url = apiBaseUrl || getApiUrl();
149
+ const frontendUrl = appUrl || getAppUrl();
150
+
151
+ // Step 1: Start device flow (already normalized in startDeviceFlow)
152
+ const startResponse = await startDeviceFlow(url);
153
+ const { deviceCode, userCode, verificationUrl, expiresIn, interval } = startResponse;
154
+
155
+ // Step 2: Display code to user
156
+ // Use verificationUrl from server, but replace base URL if MELDOC_APP_URL is set
157
+ let displayUrl = verificationUrl;
158
+
159
+ // Check if verificationUrl already contains code in query parameter
160
+ let urlHasCode = false;
161
+ try {
162
+ const urlObj = new URL(verificationUrl);
163
+ if (urlObj.searchParams.has('code')) {
164
+ urlHasCode = true;
165
+ }
166
+ } catch (e) {
167
+ // URL parsing failed, continue
168
+ }
169
+
170
+ if (process.env.MELDOC_APP_URL || appUrl) {
171
+ // Replace the base URL in verificationUrl with the configured app URL
172
+ try {
173
+ const urlObj = new URL(verificationUrl);
174
+ const appUrlObj = new URL(frontendUrl);
175
+ // Keep the path and query from verificationUrl, but use app URL origin
176
+ displayUrl = `${appUrlObj.origin}${urlObj.pathname}${urlObj.search}`;
177
+ } catch (e) {
178
+ // If URL parsing fails, use verificationUrl as is
179
+ displayUrl = verificationUrl;
180
+ }
181
+ }
182
+
183
+ // If URL doesn't have code, add it as path parameter
184
+ let fullUrl = displayUrl;
185
+ if (!urlHasCode) {
186
+ // Add code as path parameter (format: /device/CODE)
187
+ if (displayUrl.endsWith('/')) {
188
+ fullUrl = `${displayUrl}${userCode}`;
189
+ } else {
190
+ fullUrl = `${displayUrl}/${userCode}`;
191
+ }
192
+ }
193
+
194
+ onCodeDisplay(fullUrl, userCode);
195
+
196
+ // Step 3: Poll for approval
197
+ const startTime = Date.now();
198
+ const expiresAt = startTime + (expiresIn * 1000);
199
+
200
+ while (Date.now() < expiresAt) {
201
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
202
+
203
+ const pollResponse = await pollDeviceFlow(deviceCode, url);
204
+
205
+ if (pollResponse.status === 'approved') {
206
+ // Validate that we have required fields
207
+ if (!pollResponse.accessToken) {
208
+ throw new Error(`Invalid poll response: missing accessToken. Response: ${JSON.stringify(pollResponse)}`);
209
+ }
210
+
211
+ // Step 4: Save credentials
212
+ // Note: refreshToken is optional - if server doesn't provide it, auto-refresh won't work
213
+ const credentials = {
214
+ type: 'user_session',
215
+ apiBaseUrl: url,
216
+ user: pollResponse.user,
217
+ tokens: {
218
+ accessToken: pollResponse.accessToken,
219
+ accessExpiresAt: pollResponse.expiresAt || new Date(Date.now() + 3600000).toISOString(), // Default 1 hour if missing
220
+ refreshToken: pollResponse.refreshToken || null // Optional - server may not provide refreshToken
221
+ },
222
+ updatedAt: new Date().toISOString()
223
+ };
224
+
225
+ // Note: refreshToken is optional, but if provided, auto-refresh will work
226
+
227
+ // Debug logging
228
+ if (process.env.LOG_LEVEL === 'DEBUG') {
229
+ logger.debug(`Saving credentials: ${JSON.stringify({
230
+ ...credentials,
231
+ tokens: { ...credentials.tokens, accessToken: credentials.tokens.accessToken ? '[REDACTED]' : null }
232
+ }, null, 2)}`);
233
+ }
234
+
235
+ writeCredentials(credentials);
236
+
237
+ if (onStatusChange) {
238
+ onStatusChange('approved');
239
+ }
240
+
241
+ return credentials;
242
+ } else if (pollResponse.status === 'denied') {
243
+ if (onStatusChange) {
244
+ onStatusChange('denied');
245
+ }
246
+ throw new Error('Login denied by user');
247
+ } else if (pollResponse.status === 'expired') {
248
+ if (onStatusChange) {
249
+ onStatusChange('expired');
250
+ }
251
+ throw new Error('Device code expired');
252
+ }
253
+ // status === 'pending' - continue polling
254
+ }
255
+
256
+ throw new Error('Device code expired');
257
+ }
258
+
259
+ module.exports = {
260
+ startDeviceFlow,
261
+ pollDeviceFlow,
262
+ deviceFlowLogin
263
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,105 @@
1
+ const chalk = require('chalk');
2
+
3
+ /**
4
+ * Beautiful console logger with colors and emojis
5
+ */
6
+ const logger = {
7
+ /**
8
+ * Success message (green)
9
+ */
10
+ success: (message) => {
11
+ console.log(chalk.green('✓'), chalk.greenBright(message));
12
+ },
13
+
14
+ /**
15
+ * Error message (red)
16
+ */
17
+ error: (message) => {
18
+ console.error(chalk.red('✗'), chalk.redBright(message));
19
+ },
20
+
21
+ /**
22
+ * Warning message (yellow)
23
+ */
24
+ warn: (message) => {
25
+ console.warn(chalk.yellow('⚠'), chalk.yellowBright(message));
26
+ },
27
+
28
+ /**
29
+ * Info message (blue)
30
+ */
31
+ info: (message) => {
32
+ console.log(chalk.blue('ℹ'), chalk.blueBright(message));
33
+ },
34
+
35
+ /**
36
+ * Debug message (gray)
37
+ */
38
+ debug: (message) => {
39
+ console.error(chalk.gray('🔍'), chalk.gray(message));
40
+ },
41
+
42
+ /**
43
+ * Log message with custom prefix
44
+ */
45
+ log: (message) => {
46
+ console.log(message);
47
+ },
48
+
49
+ /**
50
+ * Highlighted text (cyan)
51
+ */
52
+ highlight: (text) => {
53
+ return chalk.cyan.bold(text);
54
+ },
55
+
56
+ /**
57
+ * URL formatting
58
+ */
59
+ url: (url) => {
60
+ return chalk.cyan.underline(url);
61
+ },
62
+
63
+ /**
64
+ * Code formatting
65
+ */
66
+ code: (code) => {
67
+ return chalk.bgGray.white.bold(` ${code} `);
68
+ },
69
+
70
+ /**
71
+ * Label formatting
72
+ */
73
+ label: (label) => {
74
+ return chalk.gray(label);
75
+ },
76
+
77
+ /**
78
+ * Value formatting
79
+ */
80
+ value: (value) => {
81
+ return chalk.white.bold(value);
82
+ },
83
+
84
+ /**
85
+ * Section header
86
+ */
87
+ section: (title) => {
88
+ console.log('\n' + chalk.bold.cyan('━'.repeat(50)));
89
+ console.log(chalk.bold.cyan(' ' + title));
90
+ console.log(chalk.bold.cyan('━'.repeat(50)) + '\n');
91
+ },
92
+
93
+ /**
94
+ * List item
95
+ */
96
+ item: (text, value = null) => {
97
+ if (value !== null) {
98
+ console.log(chalk.gray(' •'), chalk.white(text), chalk.gray('→'), logger.value(value));
99
+ } else {
100
+ console.log(chalk.gray(' •'), chalk.white(text));
101
+ }
102
+ }
103
+ };
104
+
105
+ module.exports = logger;
@@ -0,0 +1,76 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { getWorkspaceAlias: getConfigWorkspaceAlias } = require('./config');
5
+
6
+ /**
7
+ * Find .meldoc.yml file in current directory or parent directories
8
+ * @param {string} startDir - Starting directory
9
+ * @returns {string|null} Path to .meldoc.yml or null if not found
10
+ */
11
+ function findRepoConfig(startDir = process.cwd()) {
12
+ let currentDir = path.resolve(startDir);
13
+ const root = path.parse(currentDir).root;
14
+
15
+ while (currentDir !== root) {
16
+ const configPath = path.join(currentDir, '.meldoc.yml');
17
+ if (fs.existsSync(configPath)) {
18
+ return configPath;
19
+ }
20
+ currentDir = path.dirname(currentDir);
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ /**
27
+ * Read workspace alias from .meldoc.yml
28
+ * @param {string} configPath - Path to .meldoc.yml
29
+ * @returns {string|null} Workspace alias or null
30
+ */
31
+ function readRepoConfig(configPath) {
32
+ try {
33
+ const content = fs.readFileSync(configPath, 'utf8');
34
+ const config = yaml.load(content);
35
+ return config?.context?.workspace || null;
36
+ } catch (error) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Resolve workspace alias for request
43
+ * Priority:
44
+ * 1. Repo config (.meldoc.yml) - optional, can be skipped
45
+ * 2. Global config (~/.meldoc/config.json)
46
+ * 3. No header (let server choose automatically)
47
+ * @param {boolean} useRepoConfig - Whether to check repo config (optional)
48
+ * @returns {string|null} Workspace alias or null
49
+ */
50
+ function resolveWorkspaceAlias(useRepoConfig = true) {
51
+ // Step 1: Repo config (optional)
52
+ if (useRepoConfig) {
53
+ const repoConfigPath = findRepoConfig();
54
+ if (repoConfigPath) {
55
+ const repoWorkspace = readRepoConfig(repoConfigPath);
56
+ if (repoWorkspace) {
57
+ return repoWorkspace;
58
+ }
59
+ }
60
+ }
61
+
62
+ // Step 2: Global config
63
+ const configWorkspace = getConfigWorkspaceAlias();
64
+ if (configWorkspace) {
65
+ return configWorkspace;
66
+ }
67
+
68
+ // Step 3: No workspace alias
69
+ return null;
70
+ }
71
+
72
+ module.exports = {
73
+ findRepoConfig,
74
+ readRepoConfig,
75
+ resolveWorkspaceAlias
76
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@meldocio/mcp-stdio-proxy",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "MCP stdio proxy for meldoc - connects Claude Desktop to meldoc MCP API",
5
5
  "bin": {
6
6
  "meldoc-mcp": "bin/meldoc-mcp-proxy.js"
7
7
  },
8
8
  "files": [
9
9
  "bin/**",
10
+ "lib/**",
10
11
  "README.md",
11
12
  "LICENSE"
12
13
  ],
@@ -32,6 +33,8 @@
32
33
  },
33
34
  "homepage": "https://github.com/meldoc/mcp-stdio-proxy#readme",
34
35
  "dependencies": {
35
- "axios": "^1.6.0"
36
+ "axios": "^1.6.0",
37
+ "chalk": "^4.1.2",
38
+ "js-yaml": "^4.1.1"
36
39
  }
37
40
  }