Skip to content

Commit 7ab4cbd

Browse files
authored
130 publish to docker mcp registry (#156)
* fix: lazy load keytar - make it fully optional * fix: hello docker image for MCP Docker registry thing - minimal setup
1 parent 60a640a commit 7ab4cbd

File tree

4 files changed

+121
-12
lines changed

4 files changed

+121
-12
lines changed

Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM node:20-alpine
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
RUN npm ci --omit=dev
7+
8+
COPY . .
9+
RUN npm run build
10+
11+
ENTRYPOINT ["node", "dist/index.js"]
12+
CMD ["--http"]

docker-registry/server.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: ms-365-mcp-server
2+
image: mcp/ms-365-mcp-server
3+
type: server
4+
5+
meta:
6+
category: productivity
7+
tags:
8+
- microsoft
9+
- 365
10+
- office
11+
- graph-api
12+
- email
13+
- calendar
14+
- onedrive
15+
16+
about:
17+
title: Microsoft 365
18+
description: A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API
19+
icon: https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Microsoft_logo.svg/200px-Microsoft_logo.svg.png
20+
21+
source:
22+
project: https://github.com/softeria/ms-365-mcp-server
23+
commit: 09e4eb3288fc2086954d83a618a19112e3f1a9e0
24+
dockerfile: Dockerfile
25+
26+
run:
27+
allowHosts:
28+
- graph.microsoft.com:443
29+
- login.microsoftonline.com:443
30+
31+
config:
32+
secrets:
33+
- name: ms365.client_id
34+
env: MS365_MCP_CLIENT_ID
35+
example: your-azure-ad-app-client-id-here
36+
description: Azure AD App Registration Client ID
37+
- name: ms365.client_secret
38+
env: MS365_MCP_CLIENT_SECRET
39+
example: your-azure-ad-app-client-secret-here
40+
description: Azure AD App Registration Client Secret
41+
- name: ms365.tenant_id
42+
env: MS365_MCP_TENANT_ID
43+
example: common
44+
description: Tenant ID (use 'common' for multi-tenant or personal accounts)
45+
parameters:
46+
- name: org_mode
47+
env: MS365_MCP_ORG_MODE
48+
example: "true"
49+
description: Enable organization/work account mode
50+
- name: read_only
51+
env: READ_ONLY
52+
example: "true"
53+
description: Enable read-only mode (disable write operations)
54+
- name: enabled_tools
55+
env: ENABLED_TOOLS
56+
example: "mail,calendar"
57+
description: Comma-separated list of enabled tool patterns

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
"dotenv": "^17.0.1",
4141
"express": "^5.1.0",
4242
"js-yaml": "^4.1.0",
43-
"keytar": "^7.9.0",
4443
"winston": "^3.17.0",
4544
"zod": "^3.24.2"
4645
},
46+
"optionalDependencies": {
47+
"keytar": "^7.9.0"
48+
},
4749
"devDependencies": {
4850
"@redocly/cli": "^1.34.3",
4951
"@semantic-release/exec": "^7.1.0",

src/auth.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import type { AccountInfo, Configuration } from '@azure/msal-node';
22
import { PublicClientApplication } from '@azure/msal-node';
3-
import keytar from 'keytar';
43
import logger from './logger.js';
54
import fs, { existsSync, readFileSync } from 'fs';
65
import { fileURLToPath } from 'url';
76
import path from 'path';
87

8+
// Ok so this is a hack to lazily import keytar only when needed
9+
// since --http mode may not need it at all, and keytar can be a pain to install (looking at you alpine)
10+
let keytar: typeof import('keytar') | null = null;
11+
async function getKeytar() {
12+
if (keytar === undefined) {
13+
return null;
14+
}
15+
if (keytar === null) {
16+
try {
17+
keytar = await import('keytar');
18+
return keytar;
19+
} catch (error) {
20+
logger.info('keytar not available, using file-based credential storage');
21+
keytar = undefined as any;
22+
return null;
23+
}
24+
}
25+
return keytar;
26+
}
27+
928
interface EndpointConfig {
1029
pathPattern: string;
1130
method: string;
@@ -147,9 +166,12 @@ class AuthManager {
147166
let cacheData: string | undefined;
148167

149168
try {
150-
const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
151-
if (cachedData) {
152-
cacheData = cachedData;
169+
const kt = await getKeytar();
170+
if (kt) {
171+
const cachedData = await kt.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
172+
if (cachedData) {
173+
cacheData = cachedData;
174+
}
153175
}
154176
} catch (keytarError) {
155177
logger.warn(
@@ -177,9 +199,12 @@ class AuthManager {
177199
let selectedAccountData: string | undefined;
178200

179201
try {
180-
const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
181-
if (cachedData) {
182-
selectedAccountData = cachedData;
202+
const kt = await getKeytar();
203+
if (kt) {
204+
const cachedData = await kt.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
205+
if (cachedData) {
206+
selectedAccountData = cachedData;
207+
}
183208
}
184209
} catch (keytarError) {
185210
logger.warn(
@@ -206,7 +231,12 @@ class AuthManager {
206231
const cacheData = this.msalApp.getTokenCache().serialize();
207232

208233
try {
209-
await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
234+
const kt = await getKeytar();
235+
if (kt) {
236+
await kt.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
237+
} else {
238+
fs.writeFileSync(FALLBACK_PATH, cacheData);
239+
}
210240
} catch (keytarError) {
211241
logger.warn(
212242
`Keychain save failed, falling back to file storage: ${(keytarError as Error).message}`
@@ -224,7 +254,12 @@ class AuthManager {
224254
const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
225255

226256
try {
227-
await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
257+
const kt = await getKeytar();
258+
if (kt) {
259+
await kt.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
260+
} else {
261+
fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
262+
}
228263
} catch (keytarError) {
229264
logger.warn(
230265
`Keychain save failed for selected account, falling back to file storage: ${(keytarError as Error).message}`
@@ -403,8 +438,11 @@ class AuthManager {
403438
this.selectedAccountId = null;
404439

405440
try {
406-
await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
407-
await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
441+
const kt = await getKeytar();
442+
if (kt) {
443+
await kt.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
444+
await kt.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
445+
}
408446
} catch (keytarError) {
409447
logger.warn(`Keychain deletion failed: ${(keytarError as Error).message}`);
410448
}

0 commit comments

Comments
 (0)