# Directory Structure
```
├── .gitignore
├── icon-small.png
├── icon.png
├── LICENSE.txt
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ ├── actions
│ │ └── index.ts
│ ├── constants
│ │ └── index.ts
│ ├── index.ts
│ ├── mcp
│ │ └── index.ts
│ ├── openApiClient
│ │ └── index.ts
│ ├── resources
│ │ └── initResources.ts
│ ├── tools
│ │ ├── callTool.ts
│ │ └── utils
│ │ ├── convertTimestamps.ts
│ │ └── toTimestamps.ts
│ ├── types
│ │ ├── action.ts
│ │ ├── alibabaCloudApi.ts
│ │ └── common.ts
│ └── utils
│ ├── common.ts
│ ├── getDataWorksMcp.ts
│ ├── getDataWorksPopMcpTools.ts
│ ├── initDataWorksTools.ts
│ ├── initExtraTools.ts
│ ├── record.ts
│ └── zodToMCPSchema.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | build
3 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | [](https://mseep.ai/app/aliyun-alibabacloud-dataworks-mcp-server)
2 |
3 | # DataWorks MCP Server
4 |
5 | A Model Context Protocol (MCP) server that provides tools for AI, allowing it to interact with the DataWorks Open API through a standardized interface. This implementation is based on the Aliyun Open API and enables AI agents to perform cloud resources operations seamlessly.
6 |
7 | ## Overview
8 |
9 | This MCP server:
10 |
11 | * Interact with DataWorks Open API
12 | * Manage DataWorks resources
13 |
14 | The server implements the Model Context Protocol specification to standardize cloud resource interactions for AI agents.
15 |
16 | ## Prerequisites
17 |
18 | * Node.js (v16 or higher)
19 | * pnpm (recommended), npm, or yarn
20 | * DataWorks Open API with access key and secret key
21 |
22 | ## Installation
23 |
24 | ### Option 1: Install from npm (recommend for clients like Cursor/Cline)
25 |
26 | ```bash
27 | # Install globally
28 | npm install -g alibabacloud-dataworks-mcp-server
29 |
30 | # Or install locally in your project
31 | npm install alibabacloud-dataworks-mcp-server
32 | ```
33 |
34 | ### Option 2: Build from Source (for developers)
35 |
36 | 1. Clone this repository:
37 | ```bash
38 | git clone https://github.com/aliyun/alibabacloud-dataworks-mcp-server
39 | cd alibabacloud-dataworks-mcp-server
40 | ```
41 |
42 | 2. Install dependencies (pnpm is recommended, npm is supported):
43 | ```bash
44 | pnpm install
45 | ```
46 |
47 | 3. Build the project:
48 | ```bash
49 | pnpm run build
50 | ```
51 |
52 | 4. Development the project (by @modelcontextprotocol/inspector):
53 | ```bash
54 | pnpm run dev
55 | ```
56 | open http://localhost:5173
57 |
58 | ## Configuration
59 |
60 | ### MCP Server Configuration
61 |
62 | If you installed via npm (Option 1):
63 | ```json
64 | {
65 | "mcpServers": {
66 | "alibabacloud-dataworks-mcp-server": {
67 | "command": "npx",
68 | "args": ["alibabacloud-dataworks-mcp-server"],
69 | "env": {
70 | "REGION": "your_dataworks_open_api_region_id_here",
71 | "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_alibaba_cloud_access_key_id",
72 | "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_alibaba_cloud_access_key_secret",
73 | "TOOL_CATEGORIES": "optional_your_tool_categories_here_ex_UTILS",
74 | "TOOL_NAMES": "optional_your_tool_names_here_ex_ListProjects"
75 | },
76 | "disabled": false,
77 | "autoApprove": []
78 | }
79 | }
80 | }
81 | ```
82 |
83 | If you built from source (Option 2):
84 | ```json
85 | {
86 | "mcpServers": {
87 | "alibabacloud-dataworks-mcp-server": {
88 | "command": "node",
89 | "args": ["/path/to/alibabacloud-dataworks-mcp-server/build/index.js"],
90 | "env": {
91 | "REGION": "your_dataworks_open_api_region_id_here",
92 | "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_alibaba_cloud_access_key_id",
93 | "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_alibaba_cloud_access_key_secret",
94 | "TOOL_CATEGORIES": "optional_your_tool_categories_here_ex_SERVER_IDE_DEFAULT",
95 | "TOOL_NAMES": "optional_your_tool_names_here_ex_ListProjects"
96 | },
97 | "disabled": false,
98 | "autoApprove": []
99 | }
100 | }
101 | }
102 | ```
103 |
104 | ### Environment Setup
105 |
106 | init variables in your environment:
107 |
108 | ```env
109 | # DataWorks Configuration
110 | REGION=your_dataworks_open_api_region_id_here
111 | ALIBABA_CLOUD_ACCESS_KEY_ID=your_alibaba_cloud_access_key_id
112 | ALIBABA_CLOUD_ACCESS_KEY_SECRET=your_alibaba_cloud_access_key_secret
113 | TOOL_CATEGORIES=optional_your_tool_categories_here_ex_SERVER_IDE_DEFAULT
114 | TOOL_NAMES=optional_your_tool_names_here_ex_ListProjects
115 | ```
116 |
117 | ### Configuration Description
118 | - Use Guide Description [Link](https://www.alibabacloud.com/help/dataworks/user-guide/dataworks-mcp-server-function-usage#1ecf2a04b5ilh)
119 |
120 | ## Project Structure
121 |
122 | ```
123 | alibabacloud-dataworks-mcp-server/
124 | ├── src/
125 | │ ├── index.ts # Main entry point
126 | ├── package.json
127 | └── tsconfig.json
128 | ```
129 |
130 | ## Available Tools
131 |
132 | The MCP server provides the following DataWorks tools:
133 |
134 | See this [link](https://dataworks.data.aliyun.com/dw-pop-mcptools)
135 |
136 | ## Security Considerations
137 |
138 | * Keep your private key secure and never share it
139 | * Use environment variables for sensitive information
140 | * Regularly monitor and audit AI agent activities
141 |
142 | ## Troubleshooting
143 |
144 | If you encounter issues:
145 |
146 | 1. Verify your Aliyun Open API access key and secret key are correct
147 | 2. Check your region id is correct
148 | 3. Ensure you're on the intended network (mainnet, testnet, or devnet)
149 | 4. Verify the build was successful
150 |
151 | ## Dependencies
152 |
153 | Key dependencies include:
154 | * [@alicloud/dataworks-public20240518](https://github.com/alibabacloud-sdk-swift/dataworks-public-20240518)
155 | * [@alicloud/openapi-client](https://github.com/aliyun/darabonba-openapi)
156 |
157 | ## Contributing
158 |
159 | Contributions are welcome! Please feel free to submit a Pull Request.
160 |
161 | 1. Fork the repository
162 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
163 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
164 | 4. Push to the branch (`git push origin feature/amazing-feature`)
165 | 5. Open a Pull Request
166 |
167 | ## License
168 |
169 | This project is licensed under the Apache 2.0 License.
170 |
```
--------------------------------------------------------------------------------
/src/types/common.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface Resource {
2 | uri?: string;
3 | name?: string;
4 | description?: string;
5 | mimeType?: 'text/json' | 'text/plain' | 'image/png';
6 | }
7 |
8 | /** mcp 接口返回 */
9 | export interface DataWorksMCPResponse {
10 | jsonrpc: string;
11 | id: string;
12 | result: {
13 | /** resource 白名单 */
14 | a2reslist: string[];
15 | };
16 | }
17 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": [
14 | "src/**/*"
15 | ],
16 | "exclude": [
17 | "node_modules"
18 | ]
19 | }
```
--------------------------------------------------------------------------------
/src/tools/utils/toTimestamps.ts:
--------------------------------------------------------------------------------
```typescript
1 | import dayjs from 'dayjs';
2 | import { OpenApiClientInstance } from "../../openApiClient/index.js";
3 |
4 | export default async function toTimestamps(
5 | agent: OpenApiClientInstance,
6 | dateTimeDisplay?: string[],
7 | ) {
8 | try {
9 | const result: number[] = [];
10 | if (dateTimeDisplay) {
11 | dateTimeDisplay?.forEach?.((str) => {
12 | try {
13 | const timestamp = dayjs(str).valueOf();
14 | result.push(timestamp);
15 | } catch (e) {
16 | console.error(e);
17 | }
18 | });
19 | }
20 | return result;
21 | } catch (error: any) {
22 | throw new Error(`To timestamps failed: ${error.message}`);
23 | }
24 | }
25 |
```
--------------------------------------------------------------------------------
/src/tools/utils/convertTimestamps.ts:
--------------------------------------------------------------------------------
```typescript
1 | import dayjs from 'dayjs';
2 | import { OpenApiClientInstance } from "../../openApiClient/index.js";
3 |
4 | export default async function convertTimestamps(
5 | agent: OpenApiClientInstance,
6 | timestamps?: number[],
7 | format?: string,
8 | ) {
9 | try {
10 | const result: string[] = [];
11 | if (timestamps) {
12 | if (format) {
13 | timestamps?.forEach?.((timestamp) => {
14 | try {
15 | const date = new Date(timestamp);
16 | const display = dayjs(date).format(format || 'YYYY-MM-DD');
17 | result.push(display);
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | });
22 | };
23 | }
24 | return result;
25 | } catch (error: any) {
26 | throw new Error(`Convert timestamps failed: ${error.message}`);
27 | }
28 | }
29 |
```
--------------------------------------------------------------------------------
/src/utils/record.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fetch from 'node-fetch';
2 | import { dataWorksRecordUrl } from '../constants/index.js';
3 |
4 | /** 记录是否失败 */
5 | export default async function record(options: {
6 | success?: boolean;
7 | error?: string;
8 | toolName?: string;
9 | resourceUri?: string;
10 | version?: string;
11 | requestId?: string;
12 | } = {}) {
13 | try {
14 | await fetch(`${dataWorksRecordUrl}?method=report&requestId=${encodeURIComponent(options?.requestId || '')}&error=${encodeURIComponent(String(options?.error || ''))}&api=${encodeURIComponent(options?.toolName || '')}&type=${encodeURIComponent(options?.resourceUri ? 'resource' : options?.toolName ? 'tool' : '')}&resourceUri=${encodeURIComponent(options?.resourceUri || '')}&version=${encodeURIComponent(options?.version || '')}&success=${encodeURIComponent(options?.success || '')}&isInner=false`);
15 | console.debug('Success record');
16 | } catch (e) {
17 | console.error('Failed to record:', e);
18 | }
19 | }
```
--------------------------------------------------------------------------------
/src/utils/getDataWorksMcp.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs';
2 | import fetch from 'node-fetch';
3 | import { isPreMode, parseJSONString } from "./common.js";
4 | import { dataWorksMcpUrl, dataWorksPreMcpUrl } from '../constants/index.js';
5 | import { DataWorksMCPResponse } from '../types/common.js';
6 |
7 | /** 获取 dw mcp 接口 */
8 | export default async function getDataWorksMcp(options?: {}) {
9 | const isPre = isPreMode();
10 |
11 | // 如果是预发环境,支持本地文件
12 | const fileUri = process.env.MCP_FILE_URI || (isPre ? dataWorksPreMcpUrl : dataWorksMcpUrl);
13 |
14 | let dwMcpRes;
15 | try {
16 | if (!fileUri?.startsWith?.('http')) {
17 | // local file
18 | const fileContent = fs.readFileSync(fileUri, 'utf8');
19 | dwMcpRes = parseJSONString(fileContent);
20 | } else {
21 | // http file
22 | const queryRes = await fetch(fileUri);
23 | const resStr = await queryRes.text() as string;
24 | dwMcpRes = parseJSONString(resStr) as DataWorksMCPResponse;
25 | }
26 | } catch (e) {
27 | console.error('Failed to get getDataWorksMcp:', e);
28 | }
29 | return dwMcpRes;
30 | }
```
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OpenApiClientInstance from "../openApiClient/index.js";
2 | import callTool from "../tools/callTool.js";
3 | import { ActionTool } from "../types/action.js";
4 |
5 | export const getHandler = (apiKey: string, actionTool: ActionTool) => async (agent: OpenApiClientInstance, input: Record<string, any>) => {
6 | try {
7 |
8 | const response = await callTool(agent, apiKey, actionTool, input);
9 | return response;
10 |
11 | } catch (error: any) {
12 | // Handle specific Perplexity API error types
13 | if (error.response) {
14 | const { status, data } = error.response;
15 | if (status === 429) {
16 | return {
17 | statusCode: status,
18 | body: "Error: Rate limit exceeded. Please try again later.",
19 | };
20 | }
21 | return {
22 | statusCode: status,
23 | body: `Error: ${data.error?.message || error.message}`,
24 | };
25 | }
26 |
27 | return {
28 | body: `Failed to get information: ${error.message}`,
29 | };
30 | }
31 | };
32 |
33 | export type { ActionTool, ActionExample, Handler } from "../types/action.js";
34 |
```
--------------------------------------------------------------------------------
/src/utils/zodToMCPSchema.ts:
--------------------------------------------------------------------------------
```typescript
1 |
2 | import { z } from "zod";
3 |
4 | // Define the raw shape type that MCP tools expect
5 | export type MCPSchemaShape = {
6 | [key: string]: z.ZodType<any>;
7 | };
8 |
9 | // Type guards for Zod schema types
10 | function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> {
11 | return schema instanceof z.ZodOptional;
12 | }
13 |
14 | function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> {
15 | // Check both instanceof and the typeName property
16 | return (
17 | schema instanceof z.ZodObject ||
18 | (schema?._def?.typeName === 'ZodObject')
19 | );
20 | }
21 |
22 | /**
23 | * Converts a Zod object schema to a flat shape for MCP tools
24 | * @param schema The Zod schema to convert
25 | * @returns A flattened schema shape compatible with MCP tools
26 | * @throws Error if the schema is not an object type
27 | */
28 | export function zodToMCPShape(schema: z.ZodTypeAny): { result: MCPSchemaShape, keys: string[] } {
29 | if (!isZodObject(schema)) {
30 | throw new Error("MCP tools require an object schema at the top level");
31 | }
32 |
33 | const shape = schema.shape;
34 | const result: MCPSchemaShape = {};
35 |
36 | for (const [key, value] of Object.entries(shape)) {
37 | result[key] = isZodOptional(value as any) ? (value as any).unwrap() : value;
38 | }
39 |
40 | return {
41 | result,
42 | keys: Object.keys(result)
43 | };
44 | }
45 |
```
--------------------------------------------------------------------------------
/src/types/action.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { OpenApiClientInstance } from "../openApiClient/index.js";
2 | import { z } from "zod";
3 | import { ApiMethodUpperCase, ApiParameter, ApiParameterSchema } from "./alibabaCloudApi.js";
4 |
5 | /**
6 | * Example of an action with input and output
7 | */
8 | export interface ActionExample {
9 | input: Record<string, any>;
10 | output: Record<string, any>;
11 | explanation: string;
12 | }
13 |
14 | /**
15 | * Handler function type for executing the action
16 | */
17 | export type Handler = (
18 | agent: OpenApiClientInstance,
19 | input: Record<string, any>,
20 | ) => Promise<Record<string, any>>;
21 |
22 | export interface DwInputSchema extends Omit<ApiParameterSchema, 'required' | 'properties' | 'items'> {
23 | /** 有 properties 时,required 为 string[] */
24 | required?: string[];
25 | items?: DwInputSchema;
26 | properties?: { [name: string]: DwInputSchema };
27 | }
28 |
29 | /**
30 | * Cline 的市集应用在 Tool 上包了一个 Action,做进一步扩展
31 | * Main Action interface inspired by ELIZA
32 | * This interface makes it easier to implement actions across different frameworks
33 | */
34 | export interface ActionTool {
35 | /**
36 | * Unique name of the action
37 | */
38 | name: string;
39 |
40 | /**
41 | * Detailed description of what the action does
42 | */
43 | description?: string;
44 |
45 | /**
46 | * 直接写好 schema。
47 | * https://modelcontextprotocol.io/docs/concepts/tools
48 | */
49 | inputSchema?: DwInputSchema;
50 |
51 | annotations?: {
52 | path?: string;
53 | method?: ApiMethodUpperCase;
54 | /** ex 2024-05-18 */
55 | version?: string;
56 | example?: string;
57 | category?: string;
58 | pmd: {
59 | [name: string]: ApiParameter;
60 | };
61 | };
62 |
63 | // --------------- 以下为扩展 -----------------
64 |
65 | /**
66 | * Alternative names/phrases that can trigger this action
67 | */
68 | similes?: string[];
69 |
70 | /**
71 | * Array of example inputs and outputs for the action
72 | * Each inner array represents a group of related examples
73 | */
74 | examples?: ActionExample[][];
75 |
76 | /**
77 | * Zod schema for input validation
78 | */
79 | schema?: z.ZodType<any>;
80 |
81 | /**
82 | * Function that executes the action
83 | */
84 | handler?: Handler;
85 |
86 | /**
87 | * 有对应的 MCP Resource
88 | */
89 | hasMcpResource?: boolean;
90 | }
91 |
```
--------------------------------------------------------------------------------
/src/utils/getDataWorksPopMcpTools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs';
2 | import fetch from 'node-fetch';
3 | import { ActionTool } from "../types/action.js";
4 | import { isPreMode, parseJSONString } from "./common.js";
5 | import { dataWorksPopMcpToolsUrl, dataWorksPrePopMcpToolsUrl } from '../constants/index.js';
6 |
7 | export default async function getDataWorksPopMcpTools(options?: { categories?: string[]; names?: string[]; }) {
8 | const isPre = isPreMode();
9 | // 如果是预发环境,支持本地文件
10 | const toolFileUri = process.env.TOOL_FILE_URI || (isPre ? dataWorksPrePopMcpToolsUrl : dataWorksPopMcpToolsUrl);
11 |
12 | let dataWorksPopMcpTools: ActionTool[] = [];
13 | try {
14 | if (!toolFileUri?.startsWith?.('http')) {
15 | // local file
16 | const fileContent = fs.readFileSync(toolFileUri, 'utf8');
17 | dataWorksPopMcpTools = parseJSONString(fileContent);
18 |
19 | // 如果有传入 categories 只挑有列的
20 | const categories = (options?.categories || process.env?.TOOL_CATEGORIES?.split?.(',')) || [];
21 | if (categories?.length) {
22 | dataWorksPopMcpTools = dataWorksPopMcpTools.filter((item) => {
23 | return categories?.includes(item?.annotations?.category || '');
24 | });
25 | }
26 |
27 | // 如果有传入 names 只挑有列的
28 | const names = (options?.names || process.env?.TOOL_NAMES?.split?.(',')) || [];
29 | if (names?.length) {
30 | dataWorksPopMcpTools = dataWorksPopMcpTools.filter((item) => {
31 | return names?.includes(item?.name || '');
32 | });
33 | }
34 |
35 | } else {
36 | // http file
37 |
38 | // 接口过滤
39 | const categories = (options?.categories?.join?.(',') || process.env?.TOOL_CATEGORIES) || '';
40 | const names = (options?.names?.join?.(',') || process.env?.TOOL_NAMES) || '';
41 | let _params = '';
42 | if (categories) _params += `categories=${encodeURIComponent(categories)}`;
43 | if (names) _params += `${_params ? '&' : ''}names=${encodeURIComponent(names)}`;
44 | if (_params) _params = `?${_params}`;
45 | const queryRes = await fetch(`${toolFileUri}${_params}`);
46 | const queryResStr = await queryRes.text() as string;
47 | dataWorksPopMcpTools = parseJSONString(queryResStr) as ActionTool[];
48 | }
49 | } catch (e) {
50 | console.error('Failed to get dataWorksPopMcpTools:', e);
51 | }
52 | return dataWorksPopMcpTools;
53 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "alibabacloud-dataworks-mcp-server",
3 | "version": "1.0.43",
4 | "description": "DataWorks MCP Server: Export the DataWorks Open API to MCP Server, allowing clients that can run MCP Server to use DataWorks Open API through AI.",
5 | "main": "build/index.js",
6 | "module": "build/index.js",
7 | "type": "module",
8 | "icon": "icon.png",
9 | "bin": {
10 | "alibabacloud-dataworks-mcp-server": "./build/index.js"
11 | },
12 | "scripts": {
13 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
14 | "start": "pnpm run build; REGION=cn-shanghai node build/index.js",
15 | "dev": "pnpm run build; npx @modelcontextprotocol/inspector -e NODE_ENV=development -e REGION=cn-shanghai -e ALIBABA_CLOUD_ACCESS_KEY_ID=your_aliyun_key_id -e ALIBABA_CLOUD_ACCESS_KEY_SECRET=your_aliyun_key_secret node build/index.js",
16 | "restart": "pnpm run build; REGION=cn-shanghai node build/index.js",
17 | "pre-start": "pnpm run build; REGION=cn-shanghai NODE_ENV=development VERBOSE=true node build/index.js"
18 | },
19 | "files": [
20 | "build"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "[email protected]:aliyun/alibabacloud-dataworks-mcp-server.git"
25 | },
26 | "keywords": [
27 | "alibaba",
28 | "aliyun",
29 | "cloud",
30 | "computing",
31 | "dataworks",
32 | "big data",
33 | "mcp",
34 | "ai",
35 | "open api",
36 | "dataworks-mcp"
37 | ],
38 | "author": "DataWorks",
39 | "license": "Apache-2.0",
40 | "dependencies": {
41 | "@alicloud/credentials": "2.4.2",
42 | "@alicloud/dataworks-public20240518": "^6.0.2",
43 | "@alicloud/openapi-client": "^0.4.13",
44 | "@alicloud/openapi-util": "^0.3.2",
45 | "@alicloud/tea-typescript": "^1.8.0",
46 | "@alicloud/tea-util": "^1.4.10",
47 | "@modelcontextprotocol/sdk": "^1.5.0",
48 | "@modelcontextprotocol/server-filesystem": "2025.3.28",
49 | "bignumber.js": "^9.1.0",
50 | "dayjs": "^1.11.13",
51 | "dotenv": "^16.4.7",
52 | "express": "4.20.0",
53 | "lodash": "^4.17.21",
54 | "lossless-json": "^4.0.2",
55 | "node-fetch": "3.3.2",
56 | "openai": "^4.77.0",
57 | "path": "^0.12.7",
58 | "uuid": "9.0.1",
59 | "zod": "^3.24.2"
60 | },
61 | "devDependencies": {
62 | "@anthropic-ai/sdk": "^0.39.0",
63 | "@modelcontextprotocol/inspector": "^0.6.0",
64 | "@types/bignumber.js": "^5.0.4",
65 | "@types/lodash": "^4.17.16",
66 | "@types/node": "^22.13.4",
67 | "tailwindcss": "^3.0.0",
68 | "typescript": "^5.7.3"
69 | },
70 | "resolutions": {
71 | "@alicloud/credentials": "2.4.2",
72 | "uuid": "9.0.1"
73 | },
74 | "packageManager": "[email protected]",
75 | "publishConfig": {
76 | "registry": "https://registry.npmjs.org"
77 | },
78 | "homepage": "https://github.com/aliyun/alibabacloud-dataworks-mcp-server.git",
79 | "bugs": {
80 | "url": "https://github.com/aliyun/alibabacloud-dataworks-mcp-server/issues",
81 | "mail": ""
82 | },
83 | "tnpm": {
84 | "lockfile": "enable",
85 | "mode": "npm"
86 | }
87 | }
```
--------------------------------------------------------------------------------
/src/resources/initResources.ts:
--------------------------------------------------------------------------------
```typescript
1 | import record from '../utils/record.js';
2 | import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3 | import { isVerboseMode, getEnvInfo, getMcpResourceName, toJSONString } from '../utils/common.js';
4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5 | import { ActionTool } from '../types/action.js';
6 | import { DataWorksMCPResponse } from '../types/common.js';
7 |
8 | /**
9 | * init resources of this MCP server
10 | */
11 | async function initResources(
12 | server: McpServer['server'],
13 | dataWorksPopMcpTools: ActionTool[],
14 | dataworksMcpRes?: DataWorksMCPResponse,
15 | ) {
16 |
17 | try {
18 |
19 | // Resource file
20 |
21 | // DW 资源的白名单
22 | const dwWhiteList: string[] = dataworksMcpRes?.result?.a2reslist || [];
23 |
24 | server?.setRequestHandler(ListResourcesRequestSchema, async () => {
25 | const resourceList = dataWorksPopMcpTools?.filter((item) => dwWhiteList?.includes?.(item?.name))?.map?.((item) => {
26 | return {
27 | uri: item?.name,
28 | name: getMcpResourceName({ toolName: item?.name }),
29 | description: `${item?.name}的定义详情,如接口返回范例描述,输入参数范例等`,
30 | mimeType: "text/json",
31 | }
32 | }) || [];
33 | return {
34 | resources: resourceList,
35 | };
36 | });
37 |
38 | server?.setRequestHandler?.(ReadResourceRequestSchema, async (request) => {
39 | const uri = request?.params?.uri;
40 | try {
41 | if (uri?.startsWith?.('http')) {
42 | const res = await fetch(uri);
43 | const jsonStr = await res.text() || '{}'; // 不使用 .json(),可能会丢失精度
44 |
45 | await record({ success: true, resourceUri: uri });
46 |
47 | return {
48 | contents: [
49 | {
50 | uri,
51 | mimeType: "text/json",
52 | text: jsonStr,
53 | }
54 | ]
55 | };
56 | } else {
57 |
58 | const toolInfo = dataWorksPopMcpTools?.find?.((item) => {
59 | return item?.name === uri;
60 | });
61 |
62 | if (toolInfo) {
63 |
64 | await record({ success: true, resourceUri: uri });
65 |
66 | return {
67 | contents: [
68 | {
69 | uri,
70 | mimeType: "text/json",
71 | text: toJSONString(toolInfo || {}),
72 | }
73 | ]
74 | };
75 | } else {
76 | throw new Error(`Resource not found. ${uri}`);
77 | }
78 |
79 | }
80 | } catch (e: any) {
81 | console.error(e);
82 | await record({ success: false, error: e?.message });
83 | throw new Error(`Resource not found. ${uri}`);
84 | }
85 | });
86 |
87 | } catch (error: any) {
88 | const verbose = isVerboseMode();
89 | const errorMessage = `init resources failed: ${error.message}, ${verbose ? `, env info: ${getEnvInfo()}` : ''}`;
90 | throw new Error(errorMessage);
91 | }
92 | };
93 |
94 | export default initResources;
95 |
```
--------------------------------------------------------------------------------
/src/types/alibabaCloudApi.ts:
--------------------------------------------------------------------------------
```typescript
1 |
2 | /** https://api.aliyun.com/openmeta/struct/ApiDocs */
3 |
4 |
5 | export type ApiParameterType = 'integer' | 'string' | 'date' | 'boolean' | 'array' | 'object';
6 | export type ApiParameterFormat = 'int32' | 'int64';
7 |
8 | interface ApiDirectory {
9 | id: number;
10 | title: string;
11 | type: 'directory';
12 | children: (string | ApiDirectory)[];
13 | }
14 |
15 | interface ApiProperty {
16 | id: number;
17 | title: string;
18 | type: 'directory';
19 | format: 'int64';
20 | example: string;
21 | }
22 |
23 | interface ApiSchema {
24 | title: string;
25 | description?: string;
26 | type: 'object';
27 | properties?: { [name: string]: ApiProperty };
28 | }
29 |
30 | export type ApiMethod = 'get' | 'post' | 'delete' | 'put';
31 | export type ApiMethodUpperCase = 'GET' | 'POST' | 'DELETE' | 'PUT';
32 | type ApiScheme = 'http' | 'https';
33 | type ApiSecurity = {
34 | AK: []
35 | };
36 |
37 | export interface ApiParameterSchema {
38 | description?: string;
39 | type?: ApiParameterType;
40 | items?: ApiParameterSchema;
41 | properties?: { [name: string]: ApiParameterSchema };
42 | format?: ApiParameterFormat;
43 | required?: boolean;
44 | example?: string;
45 | }
46 |
47 | export interface ApiParameter {
48 | name?: string;
49 | /** https://help.aliyun.com/zh/sdk/developer-reference/generalized-call-node-js */
50 | in: 'query' | 'body' | 'formData' | 'byte';
51 | /** 【style=repeatList】时,数组的序列化方式为XXX.N的形式,例如:Instance.1=i-instance1&Instance.2=i-instance2, 需要配置元素最小值,最大值,根据需要开启repeatList参数校验,连续性校验 */
52 | style: 'json' | 'repeatList';
53 | schema?: ApiParameterSchema;
54 | }
55 |
56 | interface ApiResponseSchemaProperty {
57 | title: string;
58 | description: string;
59 | type: 'integer';
60 | format: 'int64';
61 | example: string;
62 | }
63 |
64 | interface ApiResponseSchema {
65 | title: string;
66 | description: string;
67 | type: 'object';
68 | properties: { [name: string]: ApiResponseSchemaProperty };
69 | }
70 |
71 | interface ApiResponse {
72 | schema: ApiResponseSchema;
73 | }
74 |
75 | export interface ApiObj {
76 | title: string;
77 | description?: string;
78 | summary: string;
79 | methods: ApiMethod[];
80 | schemes: ApiScheme[];
81 | security: ApiSecurity[];
82 | deprecated: boolean;
83 | systemTags: { [name: string]: any };
84 | parameters: ApiParameter[];
85 | responses: { [code: string]: ApiResponse };
86 | staticInfo: { [name: string]: any };
87 | responseDemo?: string;
88 | }
89 |
90 | interface ApiComponent {
91 | schemas: { [name: string]: ApiSchema };
92 | title: string;
93 | type: 'directory';
94 | }
95 |
96 | interface ApiEndpoint {
97 | regionId: string;
98 | endpoint: string;
99 | }
100 |
101 | export interface AlibabaCloudOpenApiInterface {
102 | version?: string;
103 | info?: {
104 | style: 'RPC';
105 | product: 'dataworks-public';
106 | version: string;
107 | };
108 | directories?: ApiDirectory[];
109 | /** 数据结构等信息 */
110 | components?: ApiComponent[];
111 | apis?: { [name: string]: ApiObj };
112 | endpoints?: ApiEndpoint[];
113 | }
114 |
115 | export interface IAlibabaCloudOpenApiJsonResponse {
116 | statusCode?: number;
117 | headers?: { [name: string]: string };
118 | body?: any;
119 | }
120 |
121 | export interface OpenApiConfigs {
122 | style: 'ROA' | 'RPC'; // API风格
123 | action: string; // API 名称
124 | version?: string; // API版本号
125 | protocol: 'HTTPS' | 'HTTP'; // API协议
126 | method?: ApiMethodUpperCase;// 请求方法
127 | authType: 'AK';
128 | pathname: string; // 接口 PATH
129 | reqBodyType?: 'formData' | 'byte' | 'json';// 接口请求体内容格式
130 | bodyType: 'binary' | 'array' | 'string' | 'json' | 'byte'; // 接口响应体内容格式
131 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import * as dotenv from "dotenv";
4 | import OpenApiClient from "./openApiClient/index.js";
5 | import initDataWorksTools from "./utils/initDataWorksTools.js";
6 | import initExtraTools from "./utils/initExtraTools.js";
7 | import initResources from "./resources/initResources.js";
8 | import getDataWorksMcp from "./utils/getDataWorksMcp.js";
9 | import getDataWorksPopMcpTools from "./utils/getDataWorksPopMcpTools.js";
10 | import { startMcpServer } from "./mcp/index.js";
11 | import { mcpServerVersion } from './constants/index.js';
12 | import { ActionTool } from "./types/action.js";
13 | import { ServerOptions } from "@modelcontextprotocol/sdk/server/index.js";
14 |
15 | dotenv.config();
16 |
17 | // Validate required environment variables
18 | function validateEnvironment() {
19 | const requiredEnvVars = {};
20 |
21 | const missingVars = Object.entries(requiredEnvVars)
22 | .filter(([_, value]) => !value)
23 | .map(([key]) => key);
24 |
25 | if (missingVars.length > 0) {
26 | throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
27 | }
28 | }
29 |
30 | async function main() {
31 | try {
32 | // Validate environment before proceeding
33 | validateEnvironment();
34 |
35 | // Initialize the agent with error handling
36 | const agent = await OpenApiClient.createClient();
37 |
38 | // 请求 dataworks mcp tools json
39 | const dataWorksPopMcpTools: ActionTool[] = await getDataWorksPopMcpTools();
40 |
41 | // 请求 dataworks mcp resources
42 | const dwMcpRes = await getDataWorksMcp();
43 |
44 | const mcpActions = initDataWorksTools(dataWorksPopMcpTools, dwMcpRes);
45 |
46 | // 增加额外定义的 tools
47 | const extraTools = initExtraTools() || {};
48 | Object.keys(extraTools).forEach((k) => {
49 | if (!mcpActions[k] && extraTools[k]) mcpActions[k] = extraTools[k];
50 | });
51 |
52 | const serverOptions: ServerOptions = {
53 | capabilities: {
54 | resources: {
55 | subscribe: true,
56 | listChanged: true,
57 | },
58 | tools: {},
59 | },
60 | instructions: 'Operating with DataWorks Open APIs',
61 | };
62 |
63 | // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging/
64 | if (serverOptions.capabilities && process.env.LOGGING_LEVEL) {
65 | serverOptions.capabilities.logging = {
66 | level: process.env.LOGGING_LEVEL,
67 | logFile: process.env.LOG_FILE,
68 | };
69 | }
70 |
71 | console.log('dataworks-mcp starting...');
72 |
73 | // Start the MCP server with error handling
74 | const serverWrapper = await startMcpServer(mcpActions, agent, {
75 | name: "dataworks-agent",
76 | version: mcpServerVersion,
77 | serverOptions,
78 | });
79 |
80 | const server = serverWrapper?.server;
81 |
82 | // List available resources
83 | await initResources(server, dataWorksPopMcpTools, dwMcpRes);
84 |
85 | } catch (error) {
86 | console.error('Failed to start MCP server:', error instanceof Error ? error.message : String(error));
87 | process.exit(1);
88 | }
89 | }
90 |
91 | // Handle uncaught exceptions and rejections
92 | process.on('uncaughtException', (error) => {
93 | console.error('Uncaught Exception:', error);
94 | process.exit(1);
95 | });
96 |
97 | process.on('unhandledRejection', (reason, promise) => {
98 | console.error('Unhandled Rejection at:', promise, 'reason:', reason);
99 | process.exit(1);
100 | });
101 |
102 | main();
```
--------------------------------------------------------------------------------
/src/utils/initExtraTools.ts:
--------------------------------------------------------------------------------
```typescript
1 |
2 | import convertTimestamps from '../tools/utils/convertTimestamps.js';
3 | import toTimestamps from '../tools/utils/toTimestamps.js';
4 | import { ActionTool } from '../types/action.js';
5 | import { convertInputSchemaToSchema } from './initDataWorksTools.js';
6 |
7 | /** 增加一些额外的帮助 Tools */
8 | const initExtraTools = (options?: { categories?: string[]; names?: string[]; }) => {
9 |
10 | const actionMap: { [name: string]: ActionTool } = {};
11 |
12 | try {
13 |
14 | // 将 timestamp 转成 date */
15 | let actionKey = 'ConvertTimestamps';
16 | let action: ActionTool = {
17 | name: actionKey,
18 | description: '将时间戳转成日期或时间。返回内容如果有时间戳,透过此Tool显示成为日期或时间。',
19 | schema: convertInputSchemaToSchema({
20 | type: 'object',
21 | properties: {
22 | Timestamps: {
23 | type: 'array',
24 | description: '时间戳数组',
25 | items: {
26 | type: 'integer',
27 | description: '时间戳,如:1743422516765',
28 | }
29 | },
30 | Format: {
31 | type: 'string',
32 | description: '日期格式,如:YYYY-MM-DD HH:mm:ss',
33 | },
34 | },
35 | required: ['Timestamps'],
36 | }),
37 | handler: async (agent, input) => {
38 | try {
39 | const { Timestamps, Format } = input;
40 | const response = await convertTimestamps(agent, Timestamps, Format);
41 | return response as any;
42 | } catch (error: any) {
43 | // Handle specific Perplexity API error types
44 | if (error.response) {
45 | const { status, data } = error.response;
46 | if (status === 429) {
47 | return {
48 | statusCode: status,
49 | body: "Error: Rate limit exceeded. Please try again later.",
50 | };
51 | }
52 | return {
53 | statusCode: status,
54 | body: `Error: ${data.error?.message || error.message}`,
55 | };
56 | }
57 | return {
58 | body: `Failed to get information: ${error.message}`,
59 | };
60 | }
61 | }
62 | };
63 | actionMap[actionKey] = action;
64 |
65 | // 将 display 转成 timestamp */
66 | actionKey = 'ToTimestamps';
67 | action = {
68 | name: actionKey,
69 | description: '将日期或时间转成时间戳。',
70 | schema: convertInputSchemaToSchema({
71 | type: 'object',
72 | properties: {
73 | DateTimeDisplay: {
74 | type: 'array',
75 | description: '日期或时间数组',
76 | items: {
77 | type: 'string',
78 | description: '日期或时间,如:2025-01-02 或 2025-01-01 12:11:00',
79 | }
80 | },
81 | },
82 | required: ['DateTimeDisplay'],
83 | }),
84 | handler: async (agent, input) => {
85 | try {
86 | const { DateTimeDisplay } = input;
87 | const response = await toTimestamps(agent, DateTimeDisplay);
88 | return response as any;
89 | } catch (error: any) {
90 | // Handle specific Perplexity API error types
91 | if (error.response) {
92 | const { status, data } = error.response;
93 | if (status === 429) {
94 | return {
95 | statusCode: status,
96 | body: "Error: Rate limit exceeded. Please try again later.",
97 | };
98 | }
99 | return {
100 | statusCode: status,
101 | body: `Error: ${data.error?.message || error.message}`,
102 | };
103 | }
104 | return {
105 | body: `Failed to get information: ${error.message}`,
106 | };
107 | }
108 | }
109 | };
110 | actionMap[actionKey] = action;
111 |
112 | return actionMap;
113 | } catch (e) {
114 | console.error(e);
115 | return {};
116 | }
117 | };
118 |
119 | export default initExtraTools;
120 |
121 |
```
--------------------------------------------------------------------------------
/src/utils/initDataWorksTools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { getHandler } from '../actions/index.js';
2 | import { ActionTool, DwInputSchema } from '../types/action.js';
3 | import { z } from "zod";
4 | import { ApiParameter, ApiParameterSchema } from '../types/alibabaCloudApi.js';
5 | import { DataWorksMCPResponse } from '../types/common.js';
6 |
7 | export const getZObjByType = (item?: ApiParameterSchema) => {
8 |
9 | let obj: any;
10 |
11 | const type = item?.type?.toLocaleLowerCase?.();
12 |
13 | if (type?.includes?.('int')) {
14 | // obj = z.bigint();
15 | obj = z.number();
16 | } if (type?.includes?.('double') || type?.includes?.('float')) {
17 | obj = z.number();
18 | } else if (type?.includes?.('string')) {
19 | obj = z.string();
20 | } else if (type?.includes?.('boolean')) {
21 | obj = z.boolean();
22 | } else if (type?.includes?.('date')) {
23 | obj = z.date();
24 | } else if (type?.includes?.('array')) {
25 | if (item?.items) {
26 | const childrenObjType = getZObjByType(item.items);
27 | obj = z.array(childrenObjType);
28 | } else obj = z.any();
29 | } else if (type?.includes?.('object')) {
30 |
31 | if (item?.properties) {
32 | const schema: { [name: string]: any } = {};
33 | Object.keys(item?.properties || {})?.forEach?.((paramName) => {
34 | const param = item?.properties?.[paramName];
35 | if (paramName) {
36 | let obj = getZObjByType(param);
37 | if (param?.description) obj = obj?.describe?.(param?.description);
38 | if (param?.required === false) {
39 | obj = obj?.optional?.();
40 | // 有 bug 需要执行两次
41 | if (obj?.optional) obj = obj?.optional?.();
42 | }
43 | schema[paramName] = obj;
44 | }
45 | });
46 | obj = z.object(schema);
47 | } else obj = z.any();
48 |
49 | } else {
50 | obj = z.any();
51 | }
52 |
53 | return obj;
54 | }
55 |
56 | export const convertInputSchemaToSchema = (inputSchema?: DwInputSchema, apiParameters?: ApiParameter[]) => {
57 | const schema: { [name: string]: any } = {};
58 |
59 | if (!inputSchema) return z.object(schema);
60 |
61 | const propertyKeys = Object.keys(inputSchema?.properties || {});
62 | const required = inputSchema?.required || [];
63 |
64 | // 先处理 required
65 | propertyKeys?.sort?.((a, b) => {
66 | if (required?.includes?.(a) && !required?.includes?.(b)) {
67 | return -1;
68 | } else if (!required?.includes?.(a) && required?.includes?.(b)) {
69 | return 1;
70 | } else {
71 | return 0;
72 | }
73 | });
74 |
75 | propertyKeys?.forEach?.((pKey) => {
76 |
77 | const info = inputSchema?.properties?.[pKey];
78 | const description = info?.description;
79 |
80 | let obj = getZObjByType(info as ApiParameterSchema);
81 |
82 | if (description) obj = obj?.describe?.(description);
83 |
84 | if (!required?.includes?.(pKey)) {
85 | obj = obj?.optional?.();
86 | // 有 bug 需要执行两次
87 | if (obj?.optional) obj = obj?.optional?.();
88 | }
89 |
90 | schema[pKey] = obj;
91 |
92 | });
93 |
94 | return z.object(schema);
95 |
96 | };
97 |
98 | /** 此方法是将 dw mcp 接口转成 action */
99 | const initDataWorksTools = (dwTools: ActionTool[], dwMcpRes?: DataWorksMCPResponse) => {
100 |
101 | const actionMap: { [name: string]: ActionTool } = {};
102 |
103 | try {
104 |
105 | // DW 资源的白名单
106 | const dwWhiteList: string[] = dwMcpRes?.result?.a2reslist || [];
107 |
108 | dwTools?.forEach?.((t) => {
109 | const apiKey = t?.name;
110 |
111 | // 先过滤掉几个,方便调试
112 | // if (!['CreateDataServiceApi'].includes(apiKey)) return;
113 |
114 | if (apiKey) {
115 |
116 | const map: ActionTool = { ...t };
117 |
118 | if (t?.inputSchema && !t?.schema) {
119 | const apiMeta = (Object.keys(t?.annotations?.pmd || {})?.map?.((apiKey) => (t?.annotations?.pmd?.[apiKey])) || []) as ApiParameter[];
120 | map.schema = convertInputSchemaToSchema(t?.inputSchema, apiMeta);
121 | }
122 |
123 | if (!map?.handler) {
124 | map.handler = getHandler(apiKey, t) as any;
125 | }
126 |
127 | // 查看是否有对应的 MCP Resource
128 | map.hasMcpResource = dwWhiteList?.includes?.(t?.name);
129 |
130 | actionMap[t?.name] = map;
131 |
132 | }
133 |
134 | });
135 |
136 | return actionMap;
137 | } catch (e) {
138 | console.error(e);
139 | return {};
140 | }
141 | };
142 |
143 | export default initDataWorksTools;
144 |
145 |
```
--------------------------------------------------------------------------------
/src/utils/common.ts:
--------------------------------------------------------------------------------
```typescript
1 | import isNumber from 'lodash/isNumber.js';
2 | import isString from 'lodash/isString.js';
3 | import { parse, stringify } from 'lossless-json';
4 | import { BigNumber } from "bignumber.js";
5 |
6 | /**
7 | * 是 undefined 或 null
8 | * @param value
9 | * @returns
10 | */
11 | export function isEmpty(value: any) {
12 | return value === undefined || value === null;
13 | }
14 |
15 | /**
16 | * 是undefined null 空字串
17 | * @param input
18 | * @returns {boolean}
19 | */
20 | export function isEmptyStr(input: any) {
21 | return (input === null || input === undefined || input === '');
22 | }
23 |
24 | export function isPreMode() {
25 | let pre: boolean = false;
26 | try {
27 | const env = process?.env || {};
28 | pre = env.NODE_ENV === 'development';
29 | } catch (e) {
30 | console.error(e);
31 | }
32 | return pre;
33 | }
34 |
35 | export function isVerboseMode() {
36 | let verbose: boolean = false;
37 | try {
38 | const env = process?.env || {};
39 | verbose = env.VERBOSE === 'true';
40 | } catch (e) {
41 | console.error(e);
42 | }
43 | return verbose;
44 | }
45 |
46 | export function getEnvRegion() {
47 | let regionId: string = '';
48 | try {
49 | regionId = process.env.DATAWORKS_REGION || process.env.REGION || '';
50 | } catch (e) {
51 | console.error(e);
52 | }
53 | return regionId;
54 | }
55 |
56 | export function getEnvInfo() {
57 | let envInfoStr: string = '';
58 | try {
59 | const env = process?.env || {};
60 | envInfoStr = toJSONString(env);
61 | } catch (e) {
62 | console.error(e);
63 | }
64 | return envInfoStr;
65 | }
66 |
67 | /** 检查 number 是否超过最大值,如果超过就用 BigInt */
68 | export function getNumberString(v: number) {
69 | let result: number | string = v;
70 | try {
71 | if (!isNumber(v)) return result;
72 | if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
73 | result = String(v);
74 | }
75 | } catch (e) {
76 | console.error(e);
77 | }
78 | return result;
79 | }
80 |
81 | /** 检查 number 是否超过最大值,如果超过就用 BigInt */
82 | export function getNumber(v: number) {
83 | let result: number | BigInt = v;
84 | try {
85 | if (!isNumber(v)) return result;
86 | if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
87 | result = BigInt(v);
88 | }
89 | } catch (e) {
90 | console.error(e);
91 | }
92 | return result;
93 | }
94 |
95 | /** 检查 number 是否超过最大值,如果超过就用 BigNumber (string) */
96 | export function getBigNumber(v: number) {
97 | let result: number | BigNumber = v;
98 | try {
99 | if (!isNumber(v)) return result;
100 | if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
101 | result = new BigNumber(v);
102 | }
103 | } catch (e) {
104 | console.error(e);
105 | }
106 | return result;
107 | }
108 |
109 | export function getMcpResourceName(params: { toolName?: string; }) {
110 | return params?.toolName || '';
111 | }
112 |
113 | export function isBigNumber(num: number) {
114 | try {
115 | return !Number.isSafeInteger(+num);
116 | } catch (e) {
117 | console.error(e);
118 | return true;
119 | }
120 | };
121 |
122 | /** 将 string 里有大于 Number.MAX_SAFE_INTEGER 的数字转换为 特别的string */
123 | export function parseJSONForBigNumber(jsonString: string, prefix = '__big_number__') {
124 | let result;
125 | try {
126 | // 自定义 reviver 函数
127 | function bigIntReviver(key: string, value: any) {
128 | if (isNumber(value) && isBigNumber(value)) {
129 | return `${prefix}${value}`;
130 | }
131 | return value;
132 | }
133 | result = JSON.parse(jsonString, bigIntReviver);
134 | } catch (e) {
135 | console.error(e);
136 | }
137 | return result;
138 | }
139 |
140 | /** 将值开头为 __big_number__ 转回正常的值 */
141 | export function stringifyJSONForBigNumber(json: any, prefix = '__big_number__') {
142 | let result: string = '';
143 | try {
144 | function replacer(key: string, value: any) {
145 | if (isString(value) && value.startsWith(prefix)) {
146 | return value.slice(prefix.length);
147 | }
148 | return value;
149 | }
150 | result = JSON.stringify(json, replacer);
151 |
152 | } catch (e) {
153 | console.error(e);
154 | }
155 | return result;
156 | }
157 |
158 | /** 处理 big number 问题 */
159 | export function parseJSONString(jsonString: string) {
160 | let result: any;
161 | try {
162 | result = parse(jsonString);
163 | } catch (e) {
164 | console.error(e);
165 | }
166 | return result;
167 | }
168 |
169 | /** 处理 big number 问题 */
170 | export function toJSONString(json: any, replacer?: (number | string)[] | null, space?: string | number) {
171 | let result: string = '';
172 | try {
173 | result = stringify(json, replacer, space) || '';
174 | } catch (e) {
175 | console.error(e);
176 | }
177 | return result;
178 | }
179 |
```
--------------------------------------------------------------------------------
/src/mcp/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import isString from 'lodash/isString.js';
2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import { z } from "zod";
5 | import { OpenApiClientInstance } from "../openApiClient/index.js";
6 | import { MCPSchemaShape, zodToMCPShape } from "../utils/zodToMCPSchema.js";
7 | import type { ActionExample, ActionTool } from "../types/action.js";
8 | import { convertInputSchemaToSchema } from "../utils/initDataWorksTools.js";
9 | import { ServerOptions } from "@modelcontextprotocol/sdk/server/index.js";
10 | import { getMcpResourceName, toJSONString } from "../utils/common.js";
11 |
12 | /**
13 | * Creates an MCP server from a set of actions
14 | */
15 | export function createMcpServer(
16 | actions: Record<string, ActionTool>,
17 | agent: OpenApiClientInstance,
18 | options: {
19 | name: string;
20 | version: string;
21 | serverOptions?: ServerOptions;
22 | }
23 | ) {
24 |
25 | const serverOptions: ServerOptions = options?.serverOptions || {};
26 |
27 | // Create MCP server instance
28 | const serverWrapper = new McpServer({
29 | name: options.name,
30 | version: options.version,
31 | }, serverOptions);
32 |
33 | // Convert each action to an MCP tool
34 | for (const [key, action] of Object.entries(actions)) {
35 |
36 | let paramsSchema: MCPSchemaShape = {};
37 | if (action?.schema) {
38 | const { result = {} } = action?.schema ? zodToMCPShape(action.schema) : {};
39 | paramsSchema = result;
40 | } else {
41 | const { result = {} } = zodToMCPShape(convertInputSchemaToSchema(action?.inputSchema));
42 | paramsSchema = result;
43 | }
44 |
45 | console.log('Active tool', action.name);
46 |
47 | let actionDescription = action.description || '';
48 |
49 | // 如果有对应的 MCP Resource,需要放在 description 给模型提示
50 | if (action.hasMcpResource) {
51 | actionDescription += `\n*This Tool has a 'MCP Resource',please request ${getMcpResourceName({ toolName: action.name })}(MCP Resource) to get more examples for using this tool.`;
52 | }
53 |
54 | serverWrapper.tool(
55 | action.name,
56 | actionDescription,
57 | paramsSchema,
58 | async (params) => {
59 | try {
60 | // Execute the action handler with the params directly
61 | const result = await action?.handler?.(agent, params);
62 |
63 | // Format the result as MCP tool response
64 | return {
65 | content: [
66 | {
67 | type: "text",
68 | text: result ? (isString(result) ? result : toJSONString(result, null, 2)) : '',
69 | }
70 | ]
71 | };
72 | } catch (error) {
73 | console.error("error", error);
74 | // Handle errors in MCP format
75 | return {
76 | isError: true,
77 | content: [
78 | {
79 | type: "text",
80 | text: error instanceof Error ? error.message : "Unknown error occurred"
81 | }
82 | ]
83 | };
84 | }
85 | }
86 | );
87 |
88 | // Add examples as prompts if they exist
89 | if (action.examples && action.examples.length > 0) {
90 | serverWrapper.prompt(
91 | `${action.name}-examples`,
92 | {
93 | showIndex: z.string().optional().describe("Example index to show (number)")
94 | },
95 | (args) => {
96 | const showIndex = args.showIndex ? parseInt(args.showIndex) : undefined;
97 | const examples = action?.examples?.flat?.();
98 | const selectedExamples = (typeof showIndex === 'number'
99 | ? [examples?.[showIndex]]
100 | : examples) as ActionExample[];
101 |
102 | const exampleText = selectedExamples?.map((ex, idx) => `
103 | Example ${idx + 1}:
104 | Input: ${toJSONString(ex?.input, null, 2)}
105 | Output: ${toJSONString(ex?.output, null, 2)}
106 | Explanation: ${ex?.explanation}
107 | `)
108 | .join('\n');
109 |
110 | return {
111 | messages: [
112 | {
113 | role: "user",
114 | content: {
115 | type: "text",
116 | text: `Examples for ${action.name}:\n${exampleText}`
117 | }
118 | }
119 | ]
120 | };
121 | }
122 | );
123 | }
124 | }
125 |
126 | return serverWrapper;
127 | }
128 | /**
129 | * Helper to start the MCP server with stdio transport
130 | *
131 | * @param actions - The actions to expose to the MCP server
132 | * @param agent - Aliyun Open API client instance
133 | * @param options - The options for the MCP server
134 | * @returns The MCP server
135 | * @throws Error if the MCP server fails to start
136 | * @example
137 | * import { ACTIONS } from "./actions";
138 | * import { startMcpServer } from "./mcpWrapper";
139 | *
140 | * const agent = OpenApiClient.createClient({
141 | REGION: process.env.REGION || "",
142 | ALIBABA_CLOUD_ACCESS_KEY_ID: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID || "",
143 | ALIBABA_CLOUD_ACCESS_KEY_SECRET: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET || "",
144 | });
145 | *
146 | * startMcpServer(ACTIONS, agent, {
147 | * name: "dataworks-actions",
148 | * version: "1.0.0"
149 | * });
150 | */
151 | export async function startMcpServer(
152 | actions: Record<string, ActionTool>,
153 | agent: OpenApiClientInstance,
154 | options: {
155 | name: string;
156 | version: string;
157 | serverOptions?: ServerOptions;
158 | }
159 | ) {
160 | try {
161 | const serverWrapper = createMcpServer(actions, agent, options);
162 | const transport = new StdioServerTransport();
163 | await serverWrapper.connect(transport);
164 |
165 | if (process.env.LOGGING_LEVEL) {
166 | serverWrapper?.server?.sendLoggingMessage?.({
167 | level: process.env.LOGGING_LEVEL as any,
168 | data: "Server started successfully",
169 | });
170 | }
171 |
172 | return serverWrapper;
173 | } catch (error) {
174 | console.error("Error starting MCP server", error);
175 | throw error;
176 | }
177 | }
178 |
```
--------------------------------------------------------------------------------
/src/tools/callTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | // import DataWorksPublic20240518 from '@alicloud/dataworks-public20240518';
2 | // import * as DataWorksPublic20240518Classes from '@alicloud/dataworks-public20240518';
3 | import fetch from 'node-fetch';
4 | import OpenApi from '@alicloud/openapi-client';
5 | import OpenApiUtil from '@alicloud/openapi-util';
6 | import Util from '@alicloud/tea-util';
7 | import tea from '@alicloud/tea-typescript';
8 | import record from '../utils/record.js';
9 | import isNumber from 'lodash/isNumber.js';
10 | import isString from 'lodash/isString.js';
11 | import isObject from 'lodash/isObject.js';
12 | import { OpenApiClientInstance } from "../openApiClient/index.js";
13 | import { IAlibabaCloudOpenApiJsonResponse, OpenApiConfigs } from '../types/alibabaCloudApi.js';
14 | import { ActionTool } from '../types/action.js';
15 | import { isEmptyStr, isVerboseMode, getEnvInfo, toJSONString, parseJSONString, isBigNumber } from '../utils/common.js';
16 |
17 | /**
18 | * Get detailed and latest information about any topic using Perplexity AI.
19 | * @param agent Aliyun Open API instance
20 | * @param prompt Text description of the topic to get information about
21 | * @returns Object containing the generated information
22 | */
23 | async function callTool(
24 | agent: OpenApiClientInstance,
25 | apiKey: string,
26 | actionTool: ActionTool,
27 | input?: Record<string, any>,
28 | ) {
29 |
30 | let apiRequestConfigs: OpenApi.Params = {} as any;
31 | let query: any = {};
32 | let body: any = {};
33 |
34 | // API版本号
35 | const version = actionTool?.annotations?.version;
36 |
37 | try {
38 |
39 | // 原来使用特定调用的方式
40 | // import * as DataWorksPublic20240518Classes from '@alicloud/dataworks-public20240518';
41 | // const FunctionClass = (DataWorksPublic20240518Classes as any)[`${apiKey}Request`];
42 | // const request = Reflect.construct(FunctionClass, input as any);
43 | // const runtime = new Util.RuntimeOptions({});
44 | // // 把apiKey第一个大小改小写
45 | // const funcName = `${apiKey.charAt(0).toLowerCase()}${apiKey.slice(1) || ''}WithOptions`;
46 | // return await (agent as any)[funcName](request, runtime);
47 |
48 | // 使用泛化方式调用
49 | // https://help.aliyun.com/zh/sdk/developer-reference/generalized-call-node-js
50 |
51 | // path 为空就是 RPC
52 | const style = isEmptyStr(actionTool?.annotations?.path) ? 'RPC' : 'ROA';
53 |
54 | const method = actionTool?.annotations?.method;
55 |
56 | let hasInQueryParams = false;
57 | let hasInBodyParams = false;
58 | let hasInByteParams = false;
59 | let hasInFormDataParams = false;
60 |
61 | // 需要重新 assign 下
62 | const _input: any = { ...input };
63 |
64 | Object.keys(_input)?.forEach((key) => {
65 | let value = _input[key];
66 |
67 | // if (isNumber(value)) {
68 | // // 查看值有没有溢出,如果溢出了就用string
69 | // if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
70 | // value = String(value);
71 | // }
72 | // }
73 |
74 | const paramMeta = actionTool?.annotations?.pmd?.[key];
75 | if (paramMeta?.in === 'body') {
76 | hasInBodyParams = true;
77 | body[key] = value;
78 | } else if (paramMeta?.in === 'formData') {
79 | hasInFormDataParams = true;
80 | if (paramMeta?.style === 'json') {
81 | body[key] = toJSONString(value);
82 | } else {
83 | body[key] = value;
84 | }
85 | } else if (paramMeta?.in === 'byte') {
86 | hasInByteParams = true;
87 | body[key] = value;
88 | } else if (paramMeta?.in === 'query') {
89 | hasInQueryParams = true;
90 | if (paramMeta?.style === 'json') {
91 | query[key] = toJSONString(value);
92 | } else {
93 | query[key] = value;
94 | }
95 | } else {
96 | query[key] = value;
97 | }
98 | });
99 |
100 | const reqBodyType = hasInByteParams ? 'byte' : hasInFormDataParams ? 'formData' : 'json';
101 |
102 | const bodyJson = toJSONString(body);
103 | if (reqBodyType === 'byte') {
104 | body = Buffer.from(bodyJson);
105 | } else if (reqBodyType === 'json') {
106 | body = bodyJson;
107 | }
108 |
109 | const configs: OpenApiConfigs = {
110 | style, // API风格
111 | action: apiKey, // API 名称
112 | version,
113 | protocol: 'HTTPS', // API协议
114 | method, // 请求方法
115 | authType: 'AK',
116 | pathname: `/`, // 接口 PATH
117 | reqBodyType, // 接口请求体内容格式
118 | bodyType: 'string', // 使用 string,如果使用 json 在 sdk 里会有精度丢失的问题
119 | };
120 |
121 | if (['GET', 'DELETE'].includes(method || '')) delete configs.reqBodyType;
122 |
123 | apiRequestConfigs = new OpenApi.Params(configs);
124 |
125 | // GET DELETE 这边需要把 body 设定为空,不然签名不会过
126 | if (['GET', 'DELETE'].includes(method || '')) body = null;
127 |
128 | const request = new OpenApi.OpenApiRequest({ query, body });
129 | // runtime default settings https://github.com/aliyun/tea-util/blob/5f4bdebef3b57d33207b6bc44af6ed5e1a009959/ts/test/client.spec.ts#L133
130 | const runtime = new Util.RuntimeOptions({
131 | readTimeout: process.env.OPEN_API_READ_TIMEOUT ? Number(process.env.OPEN_API_READ_TIMEOUT) : 10000,
132 | connectTimeout: process.env.OPEN_API_CONNECT_TIMEOUT ? Number(process.env.OPEN_API_CONNECT_TIMEOUT) : 10000,
133 | });
134 | // 查看 https://github.com/aliyun/darabonba-openapi/blob/master/ts/src/client.ts
135 | const res = await (agent as any)?.callApi?.(apiRequestConfigs, request, runtime);
136 | let result: IAlibabaCloudOpenApiJsonResponse['body'] | null = null;
137 | try {
138 | // 当执行成功,只取 message.body 的信息
139 | const obj = res?.statusCode === 200 && res?.body ? res?.body : res;
140 | result = parseJSONString(obj);
141 | await record({ success: true, toolName: apiKey, requestId: result?.RequestId, version });
142 | } catch (e) {
143 | console.error(e);
144 | }
145 |
146 | return result;
147 |
148 | } catch (error: any) {
149 | const verbose = isVerboseMode();
150 | const errorMsg = `Call tool failed: ${error.message}, api key: ${apiKey}, api request configs: ${toJSONString(apiRequestConfigs)}, query: ${toJSONString(query)}, body: ${toJSONString(body)}${verbose ? `, env info: ${getEnvInfo()}` : ''}`;
151 | const recordErr = `Call tool failed: ${error.message}, api key: ${apiKey}, api request configs: ${toJSONString(apiRequestConfigs)}, query: ${toJSONString(query)}, body: ${toJSONString(body)}`;
152 | await record({ success: false, toolName: apiKey, version, error: recordErr });
153 | throw new Error(errorMsg);
154 | }
155 | };
156 |
157 | export default callTool;
158 |
```
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
```
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
```