Skip to content
开发文档
能力中心
应用市场
WebOffice
开发者后台

MCP Client Auth 说明

流程总览

MCP SDK Client的Python版本暂未提供Auth流程,使用Typescript SDK的v1.11.1进行认证流程演示

  1. client的sdk在收到服务端401响应时,从openapi.wps.cn获取/.well-known的认证服务信息
  2. client再与openapi.wps.cn的认证服务进行token的换取
  3. 最终sdk会在请求中携带Authorization: bearer token保证可以正常使用openapi.wps.cn的mcp功能

mcp 认证流程图

前置条件

  1. 准备open.wps.cn应用,并得到APPIDAPPKEY,参数说明见:https://open.wps.cn/documents/app-integration-dev/server/api-description/noun-description.html
  2. 本地准备好一个认证服务的回调地址,并添加到用户授权回调地址中:https://open.wps.cn/documents/app-integration-dev/guide/self-app/develop-app.html#配置安全设置
  3. 关闭接口签名中的接口签名开关
  4. 鉴权接口会为所有MCP Server能力申请权限,请确保 [工具列表] 中的必要权限均已开启

关键代码

1. 实现OAuthClientProvider

TypeScript
class MyAuthProvider implements OAuthClientProvider {
  private tokens_: OAuthTokens | undefined;
  private codeVerifier_: string | undefined;

  get redirectUrl(): string {
    return authRedirectUri;
  }

  get clientMetadata(): OAuthClientMetadata {
    return {
      redirect_uris: [],
      client_name: ""
    };
  }

  clientInformation(): OAuthClientInformation {
    return {
      client_id: appId, // 配置为应用的APPID
      client_secret: appKey, // 配置为应用的APPKEY
    };
  }

  async tokens(): Promise<OAuthTokens | undefined> {
    return this.tokens_
  }

  async saveTokens(tokens: OAuthTokens): Promise<void> {
    this.tokens_ = tokens;
    if (!this.tokens_) {
      console.log("Received new tokens:", tokens)
    }
  }

  async redirectToAuthorization(authUrl: URL): Promise<void> {
    // 这里根据环境自行处理认证及重定向,如:
    // 浏览器端可直接window.location.href = authUrl.href
    // 服务端可通知客户端代码进行处理
    console.log("Redirecting to:", authUrl.href);
  }

  async saveCodeVerifier(verifier: string): Promise<void> {
    this.codeVerifier_ = verifier;
  }

  async codeVerifier(): Promise<string> {
    if (!this.codeVerifier_) {
      throw new Error("No code verifier saved");
    }
    return this.codeVerifier_;
  }
}

2. Transport配置authProvider

TypeScript
const authProvider = new MyAuthProvider()
const transport = new SSEClientTransport(new URL(serverUrl), { authProvider })

3. 配置认证回调接口并换取token

TypeScript
app.get(`/callback`, async (req, res) => {
  console.log("Got callback");
  const code = req.query.code;
  await transport.finishAuth(code as string);
})

完成上述步骤后,token会存储在authProvider中,后续网络请求时,会自动带入header

4. 重连client并执行后续操作

TypeScript
await client.connect(transport);
await client.request({
  method: 'tools/list',
  params: {}
}, ListToolsResultSchema)

完整示例

TypeScript
import express from "express";
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import type {
  OAuthTokens,
  OAuthClientMetadata,
  OAuthClientInformation,
} from "@modelcontextprotocol/sdk/shared/auth.js";

import {
  ListToolsRequest,
  ListToolsResultSchema,
  CallToolResult,
  CallToolResultSchema,
  LoggingMessageNotificationSchema,
} from '@modelcontextprotocol/sdk/types.js';

// 需配置WPS开放平台应用的APPID和APPKEY,以及在app应用中配置授权回调地址
const appId = process.env.APPID || "my-app-id";
const appKey = process.env.APPKEY || "my-app-key";
const authRedirectUri = process.env.AUTH_CALLBACK || "http://localhost:8080/callback";

const args = process.argv.slice(2);
const port = parseInt(args[0] || '8080');
const serverUrl = args[1] || 'http://localhost:3000/mcp/sse';

function createTransport(): SSEClientTransport {
  return new SSEClientTransport(new URL(serverUrl), { authProvider });
}

class MyAuthProvider implements OAuthClientProvider {
  private tokens_: OAuthTokens | undefined;
  private codeVerifier_: string | undefined;

  get redirectUrl(): string {
    return authRedirectUri;
  }

  get clientMetadata(): OAuthClientMetadata {
    return {
      redirect_uris: [],
      client_name: ""
    };
  }

  clientInformation(): OAuthClientInformation {
    return {
      client_id: appId,
      client_secret: appKey,
    };
  }

  async tokens(): Promise<OAuthTokens | undefined> {
    return this.tokens_
  }

  async saveTokens(tokens: OAuthTokens): Promise<void> {
    this.tokens_ = tokens;
    if (!this.tokens_) {
      console.log("Received new tokens:", tokens)
    }
  }

  async redirectToAuthorization(authUrl: URL): Promise<void> {
    console.log("Redirecting to:", authUrl.href);
    // In a browser, you might do: window.location.href = authUrl.href;
  }

  async saveCodeVerifier(verifier: string): Promise<void> {
    this.codeVerifier_ = verifier;
  }

  async codeVerifier(): Promise<string> {
    if (!this.codeVerifier_) {
      throw new Error("No code verifier saved");
    }
    return this.codeVerifier_;
  }
}
const authProvider = new MyAuthProvider()

async function runServer(port: number | null, client: Client): Promise<void> { 
  
  if (port !== null) {
    const app = express();
    app.use(express.json());

    app.get(`/callback`, async (req, res) => {
      console.log("Got callback");
      const code = req.query.code;
      if (code) {
        console.log("Received code:", code);              
        const transport = createTransport();
        await transport.finishAuth(code as string);
        res.send("Authorization successful! You can close this window.");
        await client.connect(transport);
        console.log("Auto connected using Sse transport");            
        client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
          console.log(`Notification: ${notification.params.data}`);
        });
      } else {
        res.status(400).send("No authorization code received.");
      }
    })

    app.post('/toolslist', async (req, res) => {
      console.log("Received tools list request");
      const tools = await listTools(client);
      res.json(tools);
    });

    app.post('/toolscall', async (req, res) => {
      console.log("Received tools call request");
      const { name, args } = req.body;
      const tools = await callTool(client, name, args);
      res.json(tools);
    });
    
    app.listen(port, () => {
      console.log(`Server running on http://localhost:${port}`);
    });
  } else {
    console.error("Invalid port number");
    return;
  }
}

async function listTools(client: Client): Promise<Array<any>> {
  try {
    const toolsRequest: ListToolsRequest = {
      method: 'tools/list',
      params: {}
    };
    const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);

    console.log('Available tools:');
    if (toolsResult.tools.length === 0) {
      console.log('  No tools available');
      return []
    } else {
      for (const tool of toolsResult.tools) {
        console.log(`  - ${tool.name}: ${tool.description}`);
      }
      return toolsResult.tools
    }
  } catch (error) {
    console.log(`Tools not supported by this server: ${error}`);
    throw error
  }
}

async function callTool(client: Client, name: string, args: {[key: string]: string | number | boolean}): Promise<CallToolResult> {
  try {
    return client.request({
        method: 'tools/call',
        params: {
          name,
          arguments: args,
        }
    }, CallToolResultSchema)
    .catch(error => {
      console.error(`Error in tool call:`, error);
      throw error;
    });
  } catch (error) {
    console.error(`Error starting parallel notification tools:`, error);
    throw error;
  }
}


async function main(): Promise<void> {
  console.log('MCP Auth Sse Client');
  console.log('===============================');
  console.log(`Connecting to server at: ${serverUrl}`);

  const client = new Client({
    name: 'sse-client',
    version: '1.0.0'
  });
  const transport = createTransport();
  try {
    await client.connect(transport);
    console.log("Connected using Sse transport");
  } catch (error) {
    if (error instanceof UnauthorizedError) {
      await client.close();
      console.log('Need to authorize');
    } else {
      console.error('Error creating transport:', error);
      process.exit(1);
    }
  }

  await runServer(port, client);
}

main().catch((error: unknown) => {
  console.error('Error running MCP client:', error);
  process.exit(1);
});

相关文档

WPS开放平台 | 用户授权流程

MCP Authorization