npm ts

记录一次mcp server的PR

https://github.com/larksuite/lark-openapi-mcp/pull/70

scope 就是包名前面 @ 开头的命名空间,用来区分包属于哪个组织

1
2
3
@zjmcp/lark-mcp
  ↑      ↑
scope   包名

配置.npmrc

有全局和项目.npmrc

1
2
3
4
5
6

registry=https://registry.npmmirror.com            # 其他走国内源
@scope:registry=https://xxx/repository/npm-group/  # 只有 @scope 走私有源

# Nexus 认证
//xxx/repository/npm-hosted/:_auth=[base64]
1
echo -n "账号:密码" | base64

npm命令解析

下载依赖

1
npm install --registry https://registry.npmmirror.com

package.jsondependenciesdevDependencies 列的包全部下载到本地 node_modules/,不编译、不执行、不改代码。

prepare 是 npm 的生命周期钩子,npm install 的时候会自动触发,不是 npm run build 触发的。

1
2
3
npm install
  → 下载依赖
  → 自动执行 prepare   ← 这里触发了 yarn build → 报错

npm run build 直接执行 scripts.build,跟 prepare 没关系,所以单独跑 npm run build 不会有问题。

之前报 yarn: command not found 就是 npm installprepare 钩子触发导致的。

编译

1
npm run build

执行 package.jsonscripts.build 定义的命令

1
2
3
"scripts": {
  "build": "rm -rf dist && tsc"
}

就两步:

  1. rm -rf dist — 删旧的编译产物
  2. tsc — 把 src/ 下的 TypeScript 编译成 JavaScript 输出到 dist/

npx 执行流程

  • 检查本地有没有装过,没有就从 registry 下载到临时目录

    • 相当于npm install @tiangong/lark-mcp --prefix ~/.npm/_npx/<hash>/
  • 找到 package.json 里 bin 字段对应的文件执行

    • 1
      2
      3
      "bin": {
        "lark-mcp": "dist/cli.js"   //  npx 找这个
      }
      
  • 执行完不删除,下次同版本直接用缓存 , 可以用npm cache clean --force清除

执行编译,测试

1
2
3
npm run build

node dist/cli.js login xxxxx

push tgz 到私有源

1
2
3
npm run build

npm publish [--registry https://xxx/repository/npm-hosted/]

不指定源,在 package.json 里加 publishConfig,以后直接 npm publish 就行

1
2
3
4
5
6
{
  "name": "@xxx/lark-mcp",
  "publishConfig": {
    "registry": "https://xxxx/repository/npm-hosted/"
  }
}

callback 流程

sequenceDiagram
    participant User as 用户浏览器
    participant CLI as lark-mcp CLI
    participant Store as AuthStore (本地文件)
    participant Feishu as 飞书开放平台

    Note over CLI,Store: === 1. login 登录流程 ===

    CLI->>CLI: handleLogin() 启动 express server<br/>绑定 0.0.0.0:3000 (bindHost)
    CLI->>Store: registerClient(client_id_for_local_auth)
    CLI->>CLI: generatePKCEPair() 生成 codeVerifier + codeChallenge
    CLI->>Store: storeCodeVerifier(codeVerifier)
    CLI->>User: 打印 authorizeUrl<br/>http://localhost:3000/authorize?...redirect_uri=https://callbackHost/callback

    User->>CLI: GET /authorize
    CLI->>Feishu: 302 跳转飞书授权页
    User->>Feishu: 用户点击同意授权
    Feishu->>CLI: GET /callback?code=xxx
    CLI->>Store: getCodeVerifier()
    CLI->>Feishu: exchangeAuthorizationCode(code, codeVerifier)
    Feishu-->>CLI: 返回 access_token + refresh_token
    CLI->>Store: storeLocalAccessToken(access_token, appId)<br/>storeToken(AuthInfo含refreshToken)
    CLI->>User: 返回 "success, you can close this page"
    CLI->>CLI: checkTokenWithTimeout() 检测到token存在
    CLI->>CLI: ✅ 登录成功 process.exit(0)

    Note over CLI,Store: === 2. token 使用流程 ===

    CLI->>Store: getLocalAccessToken(appId)
    Store-->>CLI: 返回 access_token
    CLI->>Feishu: API请求 withUserAccessToken(access_token)<br/>例如 docx.builtin.search

    Note over CLI,Store: === 3. token 过期刷新流程 ===

    CLI->>Store: getToken(access_token)
    Store-->>CLI: 返回 AuthInfo (含 refreshToken + expiresAt)
    CLI->>CLI: isTokenExpired() 检查是否过期
    CLI->>Feishu: exchangeRefreshToken(refreshToken)
    Feishu-->>CLI: 返回新 access_token + refresh_token
    CLI->>Store: removeToken(旧token)
    CLI->>Store: storeLocalAccessToken(新token, appId)

    Note over CLI,Store: === 4. token 存储结构 ===

    Note over Store: 本地文件 storage.json<br/>{<br/>  tokens: { "access_token_xxx": AuthInfo },<br/>  localTokens: { "appId": "access_token_xxx" },<br/>  clients: { "client_id_for_local_auth": ClientInfo }<br/>}