#
tokens: 8068/50000 19/19 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── NOTES.md
├── package.json
├── README.md
├── screenshot.png
├── scripts
│   └── create-service-account.js
├── src
│   ├── auth.ts
│   ├── config.ts
│   ├── server.ts
│   └── tools
│       ├── common.ts
│       ├── get-accounts.ts
│       ├── get-folder-contents.ts
│       ├── get-issue-comments.ts
│       ├── get-issue-root-causes.ts
│       ├── get-issue-types.ts
│       ├── get-issues.ts
│       ├── get-item-versions.ts
│       ├── get-projects.ts
│       └── index.ts
├── tsconfig.json
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | build
3 | .env
4 | *.log
5 | .DS_Store
6 | Thumbs.db
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | > IMPORTANT: This project has been moved to https://github.com/autodesk-platform-services/aps-mcp-server-nodejs.
  2 | 
  3 | ---
  4 | 
  5 | <br/>
  6 | <br/>
  7 | <br/>
  8 | <br/>
  9 | <br/>
 10 | <br/>
 11 | <br/>
 12 | <br/>
 13 | 
 14 | # aps-mcp-server
 15 | 
 16 | Experimental [Model Context Protocol](https://modelcontextprotocol.io) server build with Node.js, providing access to [Autodesk Platform Services](https://aps.autodesk.com) API, with fine-grained access control using the new _Secure Service Accounts_ feature.
 17 | 
 18 | ![Screenshot](screenshot.png)
 19 | 
 20 | [YouTube Video](https://youtu.be/6DRSR9HlIds)
 21 | 
 22 | ## Development
 23 | 
 24 | ### Prerequisites
 25 | 
 26 | - [Node.js](https://nodejs.org)
 27 | - [APS app credentials](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/create-app) (must be a _Server-to-Server_ application type)
 28 | - [Provisioned access to ACC or BIM360](https://get-started.aps.autodesk.com/#provision-access-in-other-products)
 29 | 
 30 | ### Setup
 31 | 
 32 | #### Server
 33 | 
 34 | - Clone this repository
 35 | - Install dependencies: `yarn install`
 36 | - Build the TypeScript code: `yarn run build`
 37 | - Create a _.env_ file in the root folder of this project, and add your APS credentials:
 38 |     - `APS_CLIENT_ID` - your APS application client ID
 39 |     - `APS_CLIENT_SECRET` - your APS application client secret
 40 | - Create a new service account: `npx create-service-account <username> <first name> <last name>`, for example, `npx create-service-account ssa-test-user John Doe`
 41 |     - This script will output a bunch of environment variables with information about the new account:
 42 |         - `APS_SA_ID` -  your service account ID
 43 |         - `APS_SA_EMAIL` - your service account email
 44 |         - `APS_SA_KEY_ID` - your service account key ID
 45 |         - `APS_SA_PRIVATE_KEY` - your service account private key
 46 | - Add these environment variables to your _.env_ file
 47 | 
 48 | #### Autodesk Construction Cloud
 49 | 
 50 | - Register your APS application client ID as a custom integration
 51 | - Invite the service account email as a new member to your ACC project(s)
 52 | 
 53 | ### Use with Inspector
 54 | 
 55 | - Run the [Model Context Protocol Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `yarn run inspect`
 56 | - Open http://localhost:5173
 57 | - Hit `Connect` to start this MCP server and connect to it
 58 | 
 59 | ### Use with Claude Desktop
 60 | 
 61 | - Make sure you have [Claude Desktop](https://claude.ai/download) installed
 62 | - Create a Claude Desktop config file if you don't have one yet:
 63 |     - On macOS: _~/Library/Application Support/Claude/claude\_desktop\_config.json_
 64 |     - On Windows: _%APPDATA%\Claude\claude\_desktop\_config.json_
 65 | - Add this MCP server to the config, using the absolute path of the _build/server.js_ file on your system, for example:
 66 | ```json
 67 | {
 68 |     "mcpServers": {
 69 |         "autodesk-platform-services": {
 70 |             "command": "node",
 71 |             "args": [
 72 |                 "/absolute/path/to/aps-mcp-server/build/server.js"
 73 |             ]
 74 |         }
 75 |     }
 76 | }
 77 | ```
 78 | - Open Claude Desktop, and try some of the following test prompt:
 79 |     - What ACC projects do I have access to?
 80 |     - Give me a visual dashboard of all issues in project XYZ
 81 | 
 82 | > For more details on how to add MCP servers to Claude Desktop, see the [official documentation](https://modelcontextprotocol.io/quickstart/user).
 83 | 
 84 | ### Use with Visual Studio Code & Copilot
 85 | 
 86 | - Make sure you have [enabled MCP servers in Visual Studio Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_enable-mcp-support-in-vs-code)
 87 | - Create _.vscode/mcp.json_ file in your workspace, and add the following JSON to it:
 88 | 
 89 | ```json
 90 | {
 91 |     "servers": {
 92 |         "Autodesk Platform Services": {
 93 |             "type": "stdio",
 94 |             "command": "node",
 95 |             "args": [
 96 |                 "/absolute/path/to/aps-mcp-server/build/server.js"
 97 |             ]
 98 |         }
 99 |     }
100 | }
101 | ```
102 | 
103 | > For more details on how to add MCP servers to Visual Studio Code, see the [documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
104 | 
105 | ### Use with Cursor
106 | 
107 | - Create _.cursor/mcp.json_ file in your workspace, and add the following JSON to it:
108 | 
109 | ```json
110 | {
111 |   "mcpServers": {
112 |     "Autodesk Platform Services": {
113 |       "command": "node",
114 |       "args": [
115 |         "/Users/brozp/Code/Temp/aps-mcp-server-node/build/server.js"
116 |       ]
117 |     }
118 |   }
119 | }
120 | ```
121 | 
122 | > For more details on how to add MCP servers to Cursor, see the [documentation](https://docs.cursor.com/context/model-context-protocol)
123 | 
```

--------------------------------------------------------------------------------
/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/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export { getAccounts } from "./get-accounts.js";
2 | export { getProjects } from "./get-projects.js";
3 | export { getFolderContents } from "./get-folder-contents.js";
4 | export { getItemVersions } from "./get-item-versions.js";
5 | export { getIssues } from "./get-issues.js";
6 | export { getIssueTypes } from "./get-issue-types.js";
7 | export { getIssueRootCauses } from "./get-issue-root-causes.js";
8 | export { getIssueComments } from "./get-issue-comments.js";
```

--------------------------------------------------------------------------------
/NOTES.md:
--------------------------------------------------------------------------------

```markdown
1 | # TODO
2 | 
3 | ## Authentication
4 | 
5 | Currently there's a [draft proposal](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/#22-basic-oauth-21-authorization) for adding OAuth 2.0 to Model Context Protocol. For now we'll use Secure Service Accounts.
6 | 
7 | ## Fetch
8 | 
9 | For some reason, Claude Desktop is not able to use this server when it uses the built-in [fetch](https://nodejs.org/dist/latest-v22.x/docs/api/globals.html#fetch) method, perhaps because it's using an older version of Node.js. For now we're using the [node-fetch](https://www.npmjs.com/package/node-fetch) NPM module.
```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import path from "node:path";
 2 | import url from "node:url";
 3 | import dotenv from "dotenv";
 4 | 
 5 | const __filename = url.fileURLToPath(import.meta.url);
 6 | const __dirname = path.dirname(__filename);
 7 | dotenv.config({ path: path.resolve(__dirname, "..", ".env") });
 8 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID } = process.env;
 9 | const APS_SA_PRIVATE_KEY = process.env.APS_SA_PRIVATE_KEY ? Buffer.from(process.env.APS_SA_PRIVATE_KEY, "base64").toString("utf-8") : undefined;
10 | 
11 | export {
12 |     APS_CLIENT_ID,
13 |     APS_CLIENT_SECRET,
14 |     APS_SA_ID,
15 |     APS_SA_EMAIL,
16 |     APS_SA_KEY_ID,
17 |     APS_SA_PRIVATE_KEY
18 | }
```

--------------------------------------------------------------------------------
/src/tools/get-accounts.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { DataManagementClient } from "@aps_sdk/data-management";
 2 | import { getAccessToken } from "./common.js";
 3 | import type { Tool } from "./common.js";
 4 | 
 5 | const schema = {};
 6 | 
 7 | export const getAccounts: Tool<typeof schema> = {
 8 |     title: "get-accounts",
 9 |     description: "List all available Autodesk Construction Cloud accounts",
10 |     schema,
11 |     callback: async () => {
12 |         const accessToken = await getAccessToken(["data:read"]);
13 |         const dataManagementClient = new DataManagementClient();
14 |         const hubs = await dataManagementClient.getHubs({ accessToken });
15 |         if (!hubs.data) {
16 |             throw new Error("No accounts found");
17 |         }
18 |         return {
19 |             content: hubs.data.map((hub) => ({
20 |                 type: "text",
21 |                 text: JSON.stringify({ id: hub.id, name: hub.attributes?.name })
22 |             }))
23 |         };
24 |     }
25 | };
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 3 | import * as tools from "./tools/index.js";
 4 | import { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY } from "./config.js";
 5 | 
 6 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !APS_SA_ID || !APS_SA_EMAIL || !APS_SA_KEY_ID || !APS_SA_PRIVATE_KEY) {
 7 |     console.error("Missing one or more required environment variables: APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY");
 8 |     process.exit(1);
 9 | }
10 | 
11 | const server = new McpServer({ name: "autodesk-platform-services", version: "0.0.1" });
12 | for (const tool of Object.values(tools)) {
13 |     server.tool(tool.title, tool.description, tool.schema, tool.callback);
14 | }
15 | 
16 | try {
17 |     await server.connect(new StdioServerTransport());
18 | } catch (err) {
19 |     console.error("Server error:", err);
20 | }
```

--------------------------------------------------------------------------------
/src/tools/get-item-versions.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { DataManagementClient } from "@aps_sdk/data-management";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     projectId: z.string().nonempty(),
 8 |     itemId: z.string().nonempty()
 9 | };
10 | 
11 | export const getItemVersions: Tool<typeof schema> = {
12 |     title: "get-item-versions",
13 |     description: "List all versions of a document in Autodesk Construction Cloud",
14 |     schema,
15 |     callback: async ({ projectId, itemId }) => {
16 |         // TODO: add pagination support
17 |         const accessToken = await getAccessToken(["data:read"]);
18 |         const dataManagementClient = new DataManagementClient();
19 |         const versions = await dataManagementClient.getItemVersions(projectId, itemId, { accessToken });
20 |         if (!versions.data) {
21 |             throw new Error("No versions found");
22 |         }
23 |         return {
24 |             content: versions.data.map((version) => ({ type: "text", text: JSON.stringify(version) }))
25 |         };
26 |     }
27 | };
```

--------------------------------------------------------------------------------
/src/tools/get-issues.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { IssuesClient } from "@aps_sdk/construction-issues";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     projectId: z.string().nonempty()
 8 | };
 9 | 
10 | export const getIssues: Tool<typeof schema> = {
11 |     title: "get-issues",
12 |     description: "List all available projects in an Autodesk Construction Cloud account",
13 |     schema,
14 |     callback: async ({ projectId }) => {
15 |         // TODO: add pagination support
16 |         const accessToken = await getAccessToken(["data:read"]);
17 |         const issuesClient = new IssuesClient();
18 |         projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 |         const issues = await issuesClient.getIssues(projectId, { accessToken });
20 |         if (!issues.results) {
21 |             throw new Error("No issues found");
22 |         }
23 |         return {
24 |             content: issues.results.map((issue) => ({ type: "text", text: JSON.stringify(issue) }))
25 |         };
26 |     }
27 | };
```

--------------------------------------------------------------------------------
/src/tools/get-issue-types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { IssuesClient } from "@aps_sdk/construction-issues";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     projectId: z.string().nonempty()
 8 | };
 9 | 
10 | export const getIssueTypes: Tool<typeof schema> = {
11 |     title: "get-issue-types",
12 |     description: "List all issue types in an Autodesk Construction Cloud project",
13 |     schema,
14 |     callback: async ({ projectId }) => {
15 |         // TODO: add pagination support
16 |         const accessToken = await getAccessToken(["data:read"]);
17 |         const issuesClient = new IssuesClient();
18 |         projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 |         const issueTypes = await issuesClient.getIssuesTypes(projectId, { accessToken });
20 |         if (!issueTypes.results) {
21 |             throw new Error("No issue types found");
22 |         }
23 |         return {
24 |             content: issueTypes.results.map((issue) => ({ type: "text", text: JSON.stringify(issue) }))
25 |         };
26 |     }
27 | };
```

--------------------------------------------------------------------------------
/src/tools/get-projects.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { DataManagementClient } from "@aps_sdk/data-management";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     accountId: z.string().nonempty()
 8 | };
 9 | 
10 | export const getProjects: Tool<typeof schema> = {
11 |     title: "get-projects",
12 |     description: "List all available projects in an Autodesk Construction Cloud account",
13 |     schema,
14 |     callback: async ({ accountId }) => {
15 |         // TODO: add pagination support
16 |         const accessToken = await getAccessToken(["data:read"]);
17 |         const dataManagementClient = new DataManagementClient();
18 |         const projects = await dataManagementClient.getHubProjects(accountId, { accessToken });
19 |         if (!projects.data) {
20 |             throw new Error("No projects found");
21 |         }
22 |         return {
23 |             content: projects.data.map((project) => ({
24 |                 type: "text",
25 |                 text: JSON.stringify({ id: project.id, name: project.attributes?.name })
26 |             }))
27 |         };
28 |     }
29 | };
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "aps-mcp-server",
 3 |   "version": "0.0.1",
 4 |   "type": "module",
 5 |   "private": true,
 6 |   "description": "Experimental [Model Context Protocol](https://modelcontextprotocol.io) server providing access to [Autodesk Platform Services](https://aps.autodesk.com) API.",
 7 |   "author": "Petr Broz <[email protected]>",
 8 |   "bin": {
 9 |     "aps-mcp-server": "./build/server.js",
10 |     "create-service-account": "./scripts/create-service-account.js"
11 |   },
12 |   "scripts": {
13 |     "build": "tsc",
14 |     "inspect": "mcp-inspector node ./build/server.js"
15 |   },
16 |   "files": [
17 |     "build"
18 |   ],
19 |   "dependencies": {
20 |     "@aps_sdk/authentication": "^1.0.0",
21 |     "@aps_sdk/construction-issues": "^1.1.0",
22 |     "@aps_sdk/data-management": "^1.0.1",
23 |     "@modelcontextprotocol/sdk": "^1.7.0",
24 |     "dotenv": "^16.4.7",
25 |     "jsonwebtoken": "^9.0.2",
26 |     "node-fetch": "^3.3.2",
27 |     "zod": "^3.24.2"
28 |   },
29 |   "devDependencies": {
30 |     "@modelcontextprotocol/inspector": "^0.6.0",
31 |     "@types/jsonwebtoken": "^9.0.9",
32 |     "@types/node": "^22.13.10",
33 |     "typescript": "^5.8.2"
34 |   }
35 | }
36 | 
```

--------------------------------------------------------------------------------
/src/tools/get-issue-comments.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { IssuesClient } from "@aps_sdk/construction-issues";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     projectId: z.string().nonempty(),
 8 |     issueId: z.string().nonempty()
 9 | };
10 | 
11 | export const getIssueComments: Tool<typeof schema> = {
12 |     title: "get-issue-comments",
13 |     description: "Retrieves a list of comments associated with an issue in Autodesk Construction Cloud.",
14 |     schema,
15 |     callback: async ({ projectId, issueId }) => {
16 |         // TODO: add pagination support
17 |         const accessToken = await getAccessToken(["data:read"]);
18 |         const issuesClient = new IssuesClient();
19 |         projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
20 |         const comments = await issuesClient.getComments(projectId, issueId, { accessToken})
21 |         if (!comments.results) {
22 |             throw new Error("No comments found");
23 |         }
24 |         return {
25 |             content: comments.results.map((comment) => ({ type: "text", text: JSON.stringify(comment) }))
26 |         };
27 |     }
28 | };
```

--------------------------------------------------------------------------------
/src/tools/get-issue-root-causes.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { IssuesClient } from "@aps_sdk/construction-issues";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     projectId: z.string().nonempty()
 8 | };
 9 | 
10 | export const getIssueRootCauses: Tool<typeof schema> = {
11 |     title: "get-issue-root-causes",
12 |     description: "Retrieves a list of supported root cause categories and root causes that you can allocate to an issue in Autodesk Construction Cloud.",
13 |     schema,
14 |     callback: async ({ projectId }) => {
15 |         // TODO: add pagination support
16 |         const accessToken = await getAccessToken(["data:read"]);
17 |         const issuesClient = new IssuesClient();
18 |         projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 |         const rootCauses = await issuesClient.getRootCauseCategories(projectId, { accessToken });
20 |         if (!rootCauses.results) {
21 |             throw new Error("No root causes found");
22 |         }
23 |         return {
24 |             content: rootCauses.results.map((rootCause) => ({ type: "text", text: JSON.stringify(rootCause) }))
25 |         };
26 |     }
27 | };
```

--------------------------------------------------------------------------------
/src/tools/common.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ZodRawShape } from "zod";
 2 | import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
 3 | import { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY } from "../config.js";
 4 | import { getServiceAccountAccessToken } from "../auth.js";
 5 | 
 6 | export interface Tool<Args extends ZodRawShape> {
 7 |     title: string;
 8 |     description: string;
 9 |     schema: Args;
10 |     callback: ToolCallback<Args>;
11 | }
12 | 
13 | const credentialsCache = new Map<string, { accessToken: string, expiresAt: number }>();
14 | 
15 | export async function getAccessToken(scopes: string[]): Promise<string> {
16 |     const cacheKey = scopes.join("+");
17 |     let credentials = credentialsCache.get(cacheKey);
18 |     if (!credentials || credentials.expiresAt < Date.now()) {
19 |         const { access_token, expires_in } = await getServiceAccountAccessToken(APS_CLIENT_ID!, APS_CLIENT_SECRET!, APS_SA_ID!, APS_SA_KEY_ID!, APS_SA_PRIVATE_KEY!, scopes);
20 |         credentials = {
21 |             accessToken: access_token,
22 |             expiresAt: Date.now() + expires_in * 1000
23 |         };
24 |         credentialsCache.set(cacheKey, credentials);
25 |     }
26 |     return credentials.accessToken;
27 | }
```

--------------------------------------------------------------------------------
/src/tools/get-folder-contents.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { DataManagementClient } from "@aps_sdk/data-management";
 3 | import { getAccessToken } from "./common.js";
 4 | import type { Tool } from "./common.js";
 5 | 
 6 | const schema = {
 7 |     accountId: z.string().nonempty(),
 8 |     projectId: z.string().nonempty(),
 9 |     folderId: z.string().optional()
10 | };
11 | 
12 | export const getFolderContents: Tool<typeof schema> = {
13 |     title: "get-folder-contents",
14 |     description: "List contents of a project or a specific subfolder in Autodesk Construction Cloud",
15 |     schema,
16 |     callback: async ({ accountId, projectId, folderId }) => {
17 |         // TODO: add pagination support
18 |         const accessToken = await getAccessToken(["data:read"]);
19 |         const dataManagementClient = new DataManagementClient();
20 |         const contents = folderId
21 |             ? await dataManagementClient.getFolderContents(projectId, folderId, { accessToken })
22 |             : await dataManagementClient.getProjectTopFolders(accountId, projectId, { accessToken });
23 |         if (!contents.data) {
24 |             throw new Error("No contents found");
25 |         }
26 |         return {
27 |             content: contents.data.map((item) => ({ type: "text", text: JSON.stringify(item) }))
28 |         };
29 |     }
30 | };
```

--------------------------------------------------------------------------------
/scripts/create-service-account.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | import process from "node:process";
 4 | import { getClientCredentialsAccessToken, createServiceAccount, createServiceAccountPrivateKey } from "../build/auth.js";
 5 | import { APS_CLIENT_ID, APS_CLIENT_SECRET } from "../build/config.js";
 6 | 
 7 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET) {
 8 |     console.error("Please set the APS_CLIENT_ID and APS_CLIENT_SECRET environment variables.");
 9 |     process.exit(1);
10 | }
11 | const [,, userName, firstName, lastName] = process.argv;
12 | if (!userName || !firstName || !lastName) {
13 |     console.error("Usage: node create-service-account.js <userName> <firstName> <lastName>");
14 |     console.error("Example: node create-service-account.js test-robot Rob Robot");
15 |     process.exit(1);
16 | }
17 | 
18 | try {
19 |     const credentials = await getClientCredentialsAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, ["application:service_account:write", "application:service_account_key:write"]);
20 |     const { serviceAccountId, email } = await createServiceAccount(userName, firstName, lastName, credentials.access_token);
21 |     const { kid, privateKey } = await createServiceAccountPrivateKey(serviceAccountId, credentials.access_token);
22 |     console.log("Service account created successfully!");
23 |     console.log("Invite the following user to your project:", email);
24 |     console.log("Include the following environment variables to your application:");
25 |     console.log(`APS_SA_ID="${serviceAccountId}"`);
26 |     console.log(`APS_SA_EMAIL="${email}"`);
27 |     console.log(`APS_SA_KEY_ID="${kid}"`);
28 |     console.log(`APS_SA_PRIVATE_KEY="${Buffer.from(privateKey).toString("base64")}"`);
29 | } catch (err) {
30 |     console.error(err);
31 |     process.exit(1);
32 | }
```

--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fetch from "node-fetch";
  2 | import jwt from "jsonwebtoken";
  3 | 
  4 | interface Credentials {
  5 |     access_token: string;
  6 |     token_type: string;
  7 |     expires_in: number;
  8 | }
  9 | 
 10 | /**
 11 |  * Generates an access token for APS using specific grant type.
 12 |  *
 13 |  * @param clientId The client ID provided by Autodesk.
 14 |  * @param clientSecret The client secret provided by Autodesk.
 15 |  * @param grantType The grant type for the access token.
 16 |  * @param scopes An array of scopes for which the token is requested.
 17 |  * @param assertion The JWT assertion for the access token.
 18 |  * @returns A promise that resolves to the access token response object.
 19 |  * @throws If the request for the access token fails.
 20 |  */
 21 | async function getAccessToken(clientId: string, clientSecret: string, grantType: string, scopes: string[], assertion?: string): Promise<Credentials> {
 22 |     const headers = {
 23 |         "Accept": "application/json",
 24 |         "Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
 25 |         "Content-Type": "application/x-www-form-urlencoded"
 26 |     };
 27 |     const body = new URLSearchParams({
 28 |         "grant_type": grantType,
 29 |         "scope": scopes.join(" ")
 30 |     });
 31 |     if (assertion) {
 32 |         body.append("assertion", assertion);
 33 |     }
 34 |     const response = await fetch("https://developer.api.autodesk.com/authentication/v2/token", { method: "POST", headers, body });
 35 |     if (!response.ok) {
 36 |         throw new Error(`Could not generate access token: ${await response.text()}`);
 37 |     }
 38 |     const credentials = await response.json() as Credentials;
 39 |     return credentials;
 40 | }
 41 | 
 42 | /**
 43 |  * Creates a JWT assertion for OAuth 2.0 authentication.
 44 |  *
 45 |  * @param clientId The client ID of the application.
 46 |  * @param serviceAccountId The service account ID.
 47 |  * @param serviceAccountKeyId The key ID of the service account.
 48 |  * @param serviceAccountPrivateKey The private key of the service account.
 49 |  * @param scopes The scopes for the access token.
 50 |  * @returns The signed JWT assertion.
 51 |  */
 52 | function createAssertion(clientId: string, serviceAccountId: string, serviceAccountKeyId: string, serviceAccountPrivateKey: string, scopes: string[]) {
 53 |     // TODO: validate inputs
 54 |     const payload = {
 55 |         iss: clientId,
 56 |         sub: serviceAccountId,
 57 |         aud: "https://developer.api.autodesk.com/authentication/v2/token",
 58 |         exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
 59 |         scope: scopes
 60 |     };
 61 |     const options = {
 62 |         algorithm: "RS256" as jwt.Algorithm,
 63 |         header: { alg: "RS256", kid: serviceAccountKeyId }
 64 |     };
 65 |     return jwt.sign(payload, serviceAccountPrivateKey, options);
 66 | }
 67 | 
 68 | /**
 69 |  * Generates an access token for APS using client credentials ("two-legged") flow.
 70 |  *
 71 |  * @param clientId The client ID provided by Autodesk.
 72 |  * @param clientSecret The client secret provided by Autodesk.
 73 |  * @param scopes An array of scopes for which the token is requested.
 74 |  * @returns A promise that resolves to the access token response object.
 75 |  * @throws If the request for the access token fails.
 76 |  */
 77 | export async function getClientCredentialsAccessToken(clientId: string, clientSecret: string, scopes: string[]) {
 78 |     return getAccessToken(clientId, clientSecret, "client_credentials", scopes);
 79 | }
 80 | 
 81 | /**
 82 |  * Retrieves an access token for a service account using client credentials and JWT assertion.
 83 |  *
 84 |  * @param clientId The client ID for the OAuth application.
 85 |  * @param clientSecret The client secret for the OAuth application.
 86 |  * @param serviceAccountId The ID of the service account.
 87 |  * @param serviceAccountKeyId The key ID of the service account.
 88 |  * @param serviceAccountPrivateKey The private key of the service account.
 89 |  * @param scopes An array of scopes for the access token.
 90 |  * @returns A promise that resolves to the access token response object.
 91 |  * @throws If the access token could not be retrieved.
 92 |  */
 93 | export async function getServiceAccountAccessToken(clientId: string, clientSecret: string, serviceAccountId: string, serviceAccountKeyId: string, serviceAccountPrivateKey: string, scopes: string[]) {
 94 |     const assertion = createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes);
 95 |     return getAccessToken(clientId, clientSecret, "urn:ietf:params:oauth:grant-type:jwt-bearer", scopes, assertion);
 96 | }
 97 | 
 98 | /**
 99 |  * Creates a new service account with the given name.
100 |  *
101 |  * @param name The name of the service account to create (must be between 5 and 64 characters long).
102 |  * @param firstName The first name of the service account.
103 |  * @param lastName The last name of the service account.
104 |  * @param accessToken The access token for authentication.
105 |  * @returns A promise that resolves to the created service account response.
106 |  * @throws If the request to create the service account fails.
107 |  */
108 | export async function createServiceAccount(name: string, firstName: string, lastName: string, accessToken: string) {
109 |     const headers = {
110 |         "Accept": "application/json",
111 |         "Authorization": `Bearer ${accessToken}`,
112 |         "Content-Type": "application/json"
113 |     };
114 |     const body = JSON.stringify({ name, firstName, lastName });
115 |     const response = await fetch("https://developer.api.autodesk.com/authentication/v2/service-accounts", { method: "POST", headers, body });
116 |     if (!response.ok) {
117 |         throw new Error(`Could not create service account: ${await response.text()}`);
118 |     }
119 |     return response.json();
120 | }
121 | 
122 | /**
123 |  * Creates a private key for a given service account.
124 |  *
125 |  * @param serviceAccountId - The ID of the service account for which to create a private key.
126 |  * @param accessToken - The access token used for authorization.
127 |  * @returns A promise that resolves to the private key details.
128 |  * @throws If the request to create the private key fails.
129 |  */
130 | export async function createServiceAccountPrivateKey(serviceAccountId: string, accessToken: string) {
131 |     const headers = {
132 |         "Accept": "application/json",
133 |         "Authorization": `Bearer ${accessToken}`
134 |     };
135 |     const response = await fetch(`https://developer.api.autodesk.com/authentication/v2/service-accounts/${serviceAccountId}/keys`, { method: "POST", headers });
136 |     if (!response.ok) {
137 |         throw new Error(`Could not create service account private key: ${await response.text()}`);
138 |     }
139 |     return response.json();
140 | }
```