跪拜 Guibai
← Back to the summary

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:

  1. Use npm to turn the MCP Server into an installable, publishable package;
  2. Launch it in any MCP-supporting Agent client using npx / node;
  3. 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

ChatGPT Image 2026年7月21日 09_56_17.png

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)

ChatGPT Image 2026年7月21日 14_52_04.png 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:

  1. Read the Cookie value named ACCESS_TOKEN from the browser → write to session.token
  2. Also concatenate all Cookies into session.cookie → subsequent requests carry the Cookie header
  3. If session.token has a value, it also carries Authorization: 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:

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

  1. Save the configuration and restart the Agent client (or Reload MCP) → Server shows as connected
  2. Agent calls login_with_browser → Log in via local Chrome
  3. Call list_files / download_file
  4. 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


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
Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

ZV锦

Hello, I'd like to ask how the flowcharts in the article were generated?

我的刀盾

Generated by gpt image2