A Five-Year-Old Intranet App Gets an AI Sidecar via MCP and npm
Foreword
That web system, back when the project was pitched, had a PowerPoint presentation that promised the world. Now, when you open the backend, the daily active users are in the single digits—one is the tester, and the other is you refreshing the page.
Colleagues say "we should have rebuilt it ages ago," but their hands are busy asking ChatGPT: "Download the reports from this folder for me." AI can chat, write, and draw, but it can't get past the login page of your intranet. It's not that AI is incapable; it's that the old system never issued it a badge.
So-called "empowerment" sounds like a boardroom buzzword, but in practice, it boils down to one thing: Don't make the Agent reinvent a login and download mechanism; let it use your existing APIs. Users still log in via a QR code scan in the browser; the Token goes into the MCP process memory; the Agent says "list files" or "download this attachment," and it hits the exact same APIs as clicking a button on your page.
This article isn't about microservice transformations or Kubernetes. It's about how to use Node + npm + MCP to add an AI-accessible bypass to an old system that "nobody uses but can't be shut down." The old system is responsible for staying alive; the AI is responsible for doing the work—a clear division of labor, each doing what it does best.
You already have a web system: users can list and download files after logging in. The goal isn't to change the business stack, but to:
- Use npm to turn the MCP Server into an installable, publishable package;
- Launch it in any MCP-supporting Agent client using npx / node;
- Have the Agent carry the login state and call the same file list / download APIs as the web page.
MCP is an open protocol, not limited to Cursor. The same npm package can be connected to Cursor, Claude Desktop, VS Code (MCP extension), Windsurf, etc. The following uses mcp.json as an example; configuration paths differ per client, but the stdio launch command is the same.
What needs to be done?
| Step | Action |
|---|---|
| 1 | Confirm list/download APIs; configure LOGIN_URL, TOKEN_SOURCE |
| 2 | Write MCP tools: login_with_browser / list_files / download_file |
| 3 | npm install (includes playwright, do not run playwright install) |
| 4 | npm publish --registry=https://admin.npm.xxxxer.me/ |
| 5 | Configure mcp.json in the Agent client: npx -y --registry=... [email protected] |
| 6 | Agent first calls login_with_browser (local Chrome) → Token enters memory → then list files/download |
1. Aligning with the Existing System
| Page Capability | MCP Tool | Typical API |
|---|---|---|
| Login | login_with_browser |
Playwright channel: chrome, credentials stored in memory |
| File List | list_files |
GET /api/files?path= |
| Download | download_file |
GET /api/files/download?id= |
| Logout | logout / clear_auth |
POST /api/logout (optional) |
Supplementary tools: auth_status, clear_auth, logout (see §3.4 for logout).
Authentication uses the process memory written by login_with_browser; no need to configure API_TOKEN. The intranet is only accessed locally; no intranet penetration or public exposure is done.
2. What npm Does in This Chain
| npm Capability | Purpose |
|---|---|
npm install |
Installs SDK, playwright, zod |
package.json + bin |
MCP executable entry point |
npm publish |
Publishes to https://admin.npm.xxxxer.me/ |
npx -y --registry=... package@version |
Agent client launches MCP (stdio) |
The business system continues to run; MCP is a bypass independent package. See section 4 for configuration.
Common Agent Clients and Configuration Files
| Client | Configuration File Location (Windows) |
|---|---|
| Cursor | Project: <repo>\.cursor\mcp.json; Global: %USERPROFILE%\.cursor\mcp.json |
| Claude Desktop | %APPDATA%\Claude\claude_desktop_config.json |
| VS Code | User/Workspace MCP configuration (provided by extension, structure is also mcpServers) |
| Windsurf | Same as Cursor, .windsurf/mcp.json or settings panel |
Each client's UI differs, but the MCP Server section structure is consistent: command + args + env.
3. Building an MCP npm Package from Scratch
3.1 Directory
your-files-mcp/
package.json
src/server.mjs # stdio entry point
.gitignore # includes .env
Reference: mcp-download-sidecar/.
3.2 package.json
{
"name": "files-mcp-client",
"version": "0.2.0",
"type": "module",
"bin": { "files-mcp-client": "./src/server.mjs" },
"files": ["src"],
"publishConfig": { "registry": "https://admin.npm.xxxxer.me/" },
"scripts": { "start": "node src/server.mjs" },
"engines": { "node": ">=18" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"playwright": "^1.52.0",
"zod": "^3.25.28"
}
}
playwright must be in dependencies; at startup, use channel: "chrome" to call the local browser, do not run playwright install chromium.
3.3 Login and Browser: Token Retrieval and Memory Storage (Key Code)
See
mcp-download-sidecar/download-server.mjs for the full implementation. The core is four steps:
① In-process session object (not persisted to disk)
const session = { token: "", cookie: "", obtainedAt: null };
function getToken() {
return session.token; // Subsequent list_files / download_file reads from here
}
② Open local Chrome, wait for user login
const { chromium } = await import("playwright");
const { context } = await chromium.launchPersistentContext(userDataDir, {
headless: false,
channel: "chrome", // Local Chrome, does not download Chromium
});
const page = context.pages()[0] ?? (await context.newPage());
await page.goto(LOGIN_URL, { waitUntil: "domcontentloaded" });
// Poll: wait until credentials appear in localStorage / Cookie / request headers
await waitForLoginCredentials(page, context, { source: TOKEN_SOURCE, timeout });
③ Extract Token from the page (based on TOKEN_SOURCE config)
// TOKEN_SOURCE format: type:key_name
// Example: cookie:ACCESS_TOKEN | localStorage:token | header:Authorization
async function extractTokenFromPage(page, source) {
const [kind, key] = source.split(":");
if (kind === "localStorage") {
return page.evaluate((k) => localStorage.getItem(k) || "", key);
}
if (kind === "cookie") {
const hit = (await page.context().cookies()).find((c) => c.name === key);
return hit?.value || ""; // When key=ACCESS_TOKEN, gets the value of that Cookie
}
return "";
}
Example: After login, the Cookie is named ACCESS_TOKEN
TOKEN_SOURCE=cookie:ACCESS_TOKEN
"env": {
"TOKEN_SOURCE": "cookie:ACCESS_TOKEN"
}
After a successful login, the code will:
- Read the Cookie value named
ACCESS_TOKENfrom the browser → write tosession.token - Also concatenate all Cookies into
session.cookie→ subsequent requests carry theCookieheader - If
session.tokenhas a value, it also carriesAuthorization: Bearer <ACCESS_TOKEN value>
If your backend only recognizes Cookie: ACCESS_TOKEN=xxx and not Bearer, you can rely solely on session.cookie (the implementation will carry the entire string).
// Can also intercept the Authorization header sent by the page (used when TOKEN_SOURCE=header:Authorization)
let bearer = "";
context.on("request", (req) => {
const m = /^Bearer\s+(.+)$/i.exec(req.headers()["authorization"] || "");
if (m) bearer = m[1];
});
const token = (await extractTokenFromPage(page, TOKEN_SOURCE)) || bearer;
const cookieHeader = (await context.cookies())
.map((c) => `${c.name}=${c.value}`)
.join("; ");
④ Write to memory for subsequent API use
session.token = token || "";
session.cookie = cookieHeader || "";
session.obtainedAt = new Date().toISOString();
await context.close(); // Close browser; credentials remain in session
// Afterwards, list_files / download_file automatically carry auth headers
function buildHeaders() {
const headers = { Accept: "application/json, */*" };
if (session.token) headers.Authorization = `Bearer ${session.token}`;
if (session.cookie) headers.Cookie = session.cookie;
return headers;
}
await fetch(`${API_BASE}/api/files`, { headers: buildHeaders() });
Key points:
- Credentials only exist in the
sessionobject, not written to files, not placed in mcp.json - When the MCP process restarts,
sessionis cleared;login_with_browsermust be called again TOKEN_SOURCEmust match how your frontend actually stores the Token (check the key name in DevTools → Application)
Common environment variables:
API_BASE=https://your-app.example.com
LOGIN_URL=https://your-app.example.com/login
TOKEN_SOURCE=cookie:ACCESS_TOKEN # Cookie name ACCESS_TOKEN; or localStorage:token / header:Authorization
PLAYWRIGHT_CHANNEL=chrome
DOWNLOAD_DIR=F:/downloads/app-files
LIST_API_PATH=/api/files
DOWNLOAD_API_PATH=/api/files/download
LOGOUT_API_PATH=/api/logout
3.4 Logout
Logout is divided into three layers:
| Layer | Action | How to invoke |
|---|---|---|
| MCP Memory | Clear session, subsequent API calls no longer carry credentials |
clear_auth |
| Backend Session (Recommended) | Invalidate server-side ACCESS_TOKEN |
logout (configure LOGOUT_API_PATH) |
| Browser Cookie (Optional) | Cookies may still exist in local Chrome | User clicks "Logout" in browser |
Tell the Agent to "log out" → calls logout.
"env": { "LOGOUT_API_PATH": "/api/logout" }
logout flow: first calls the backend logout with current Cookie/Token (default POST) → then clears memory (memory is cleared even if the backend call fails).
| Tool | Calls Backend | Clears Memory |
|---|---|---|
clear_auth |
No | Yes |
logout |
Yes | Yes |
Verification: auth_status's ready should be false. See the lower half of the diagram "2. Logout".
4. Publishing and Connecting to Agent Client
4.1 publish
npm login --registry=https://admin.npm.xxxxer.me/
npm publish --registry=https://admin.npm.xxxxer.me/
4.2 MCP Configuration (General)
Add the following to the MCP configuration of the Agent you are using (Cursor: .cursor/mcp.json, Claude Desktop: claude_desktop_config.json, others see the table above):
{
"mcpServers": {
"files": {
"command": "npx",
"args": [
"-y",
"--registry=https://admin.npm.xxxxer.me/",
"[email protected]"
],
"env": {
"API_BASE": "https://your-system-domain",
"LOGIN_URL": "https://your-system-domain/login",
"TOKEN_SOURCE": "cookie:ACCESS_TOKEN",
"PLAYWRIGHT_CHANNEL": "chrome",
"DOWNLOAD_DIR": "F:/downloads/app-files"
}
}
}
}
Note: --registry=... is written as a single args item; pin the version; do not write API_TOKEN.
4.3 Development Phase (Before Publishing)
Change command to node, args points to the local server.mjs, env is the same as above. Switch back to npx after it works.
4.4 Usage
- Save the configuration and restart the Agent client (or Reload MCP) → Server shows as connected
- Agent calls
login_with_browser→ Log in via local Chrome - Call
list_files/download_file - Must log in again after MCP process restarts
5. Boundaries and Acceptance
Do / Don't
| Do | Don't |
|---|---|
| Call existing post-login APIs | Open unauthenticated backdoors |
| Token only in process memory | Write Token into mcp.json / Git |
| Access intranet locally | Intranet penetration, public exposure |
Acceptance
- Node ≥ 18; MCP Server shows as connected in the client
- After
login_with_browser,list_files/download_filesucceed - Files land in
DOWNLOAD_DIR -
npx -y --registry=... [email protected]can start - Repository contains no secrets
6. Common Pitfalls
| Symptom | Handling |
|---|---|
| MCP not connected / red light | npx not in the client process's PATH; use node absolute path during dev |
npx package fetch fails |
Check --registry=https://admin.npm.xxxxer.me/ |
| 401 / downloads login page HTML | Token expired or not logged in; re-run login_with_browser |
| Browser won't start | Install local Chrome/Edge; do not rely on downloading Chromium |
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
Hello, I'd like to ask how the flowcharts in the article were generated?
Generated by gpt image2