├── .changeset
├── README.md
└── config.json
├── .eslintignore
├── .github
├── actions
│ ├── branch-info
│ │ └── action.yml
│ ├── install
│ │ └── action.yml
│ ├── local-payloads
│ │ ├── README.md
│ │ └── example-pr.json
│ └── setup
│ │ └── action.yml
└── workflows
│ ├── ci-checks.yml
│ ├── ci-setup.yml
│ ├── main.yml
│ └── pr.yml
├── .gitignore
├── .nvmrc
├── .vscode
└── settings.json
├── .yarn
├── patches
│ └── @changesets-cli-npm-2.29.3-b3f38d424c.patch
├── plugins
│ └── @yarnpkg
│ │ └── plugin-engines.cjs
└── releases
│ └── yarn-4.9.1.cjs
├── .yarnrc.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps
└── web
│ ├── app.config.ts
│ ├── eslint.config.js
│ ├── package.json
│ ├── public
│ └── favicon.ico
│ ├── src
│ ├── assets
│ │ ├── app.css
│ │ └── fonts
│ │ │ └── ml
│ │ │ ├── 0-normal.woff2
│ │ │ └── 1-italic.woff2
│ ├── client.tsx
│ ├── components
│ │ └── CommandExplorer.tsx
│ ├── routeTree.gen.ts
│ ├── router.tsx
│ ├── routes
│ │ ├── __root.tsx
│ │ └── index.tsx
│ └── ssr.tsx
│ ├── tsconfig.json
│ └── wrangler.jsonc
├── nx.json
├── package.json
├── packages
├── cli
│ ├── CHANGELOG.md
│ ├── DEVELOPMENT.md
│ ├── README.md
│ ├── bin
│ │ └── index.js
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── api
│ │ │ └── index.ts
│ │ ├── commands
│ │ │ ├── install
│ │ │ │ ├── handler.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── openapi
│ │ │ │ │ ├── create-remix.ts
│ │ │ │ │ ├── generate-remix.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── negotiate-filepath.ts
│ │ │ │ │ └── types.ts
│ │ │ ├── login
│ │ │ │ └── index.ts
│ │ │ ├── logout
│ │ │ │ └── index.ts
│ │ │ ├── run
│ │ │ │ ├── handler.ts
│ │ │ │ └── index.ts
│ │ │ ├── uninstall
│ │ │ │ ├── handler.ts
│ │ │ │ └── index.ts
│ │ │ ├── upload
│ │ │ │ ├── handler.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-utils
│ │ │ │ │ ├── create-client.ts
│ │ │ │ │ └── get-tools.ts
│ │ │ │ ├── transport-definitions
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── openapi.ts
│ │ │ │ │ ├── sse.ts
│ │ │ │ │ ├── stdio.ts
│ │ │ │ │ ├── streamable-http.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ │ ├── __tests__
│ │ │ │ │ ├── mask-headers.spec.ts
│ │ │ │ │ └── mask-url.spec.ts
│ │ │ │ │ ├── config-schema.ts
│ │ │ │ │ ├── mask-headers.ts
│ │ │ │ │ ├── mask-url.ts
│ │ │ │ │ ├── parse-cmd
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ ├── index.spec.ts
│ │ │ │ │ │ ├── parse-docker-run.spec.ts
│ │ │ │ │ │ ├── parse-env-variables.spec.ts
│ │ │ │ │ │ ├── parse-generic.spec.ts
│ │ │ │ │ │ └── parse-npx.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── parse-docker-run.ts
│ │ │ │ │ ├── parse-env-variables.ts
│ │ │ │ │ ├── parse-generic.ts
│ │ │ │ │ ├── parse-npx-command.ts
│ │ │ │ │ ├── parsed-command.ts
│ │ │ │ │ └── types.ts
│ │ │ │ │ └── string.ts
│ │ │ └── whoami
│ │ │ │ └── index.ts
│ │ ├── env.ts
│ │ ├── errors
│ │ │ ├── guards.ts
│ │ │ ├── handler-error.ts
│ │ │ ├── index.ts
│ │ │ ├── printable-error.ts
│ │ │ └── ui-errors.ts
│ │ ├── index.ts
│ │ ├── libs
│ │ │ ├── cli-utils
│ │ │ │ ├── create-handler.ts
│ │ │ │ └── index.ts
│ │ │ ├── console
│ │ │ │ ├── index.ts
│ │ │ │ ├── prompts
│ │ │ │ │ ├── confirm.ts
│ │ │ │ │ ├── guards.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── select.ts
│ │ │ │ │ ├── state.ts
│ │ │ │ │ ├── text.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── url.ts
│ │ │ │ │ └── user.ts
│ │ │ │ ├── reporters
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ └── log-file.spec.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── fancy
│ │ │ │ │ │ ├── fancy.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── log-file.ts
│ │ │ │ └── types.ts
│ │ │ ├── datanaut
│ │ │ │ ├── agent.ts
│ │ │ │ ├── auth-cli.ts
│ │ │ │ ├── auth
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── server.ts
│ │ │ │ │ └── storage
│ │ │ │ │ │ ├── __tests__
│ │ │ │ │ │ └── mutex.spec.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── mutex.ts
│ │ │ │ ├── fs-config.ts
│ │ │ │ └── sdk
│ │ │ │ │ └── sdk.ts
│ │ │ ├── mcp-clients
│ │ │ │ ├── config
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ ├── read.spec.ts
│ │ │ │ │ │ ├── resolve-path.spec.ts
│ │ │ │ │ │ └── write.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── read.ts
│ │ │ │ │ ├── resolve-path.ts
│ │ │ │ │ └── write.ts
│ │ │ │ ├── errors
│ │ │ │ │ ├── guards.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── install-method-unavailable.ts
│ │ │ │ │ ├── server-conflict.ts
│ │ │ │ │ └── server-not-installed.ts
│ │ │ │ ├── get-install-filepath.ts
│ │ │ │ ├── get-install-hints.ts
│ │ │ │ ├── index.isomorphic.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── install.ts
│ │ │ │ ├── integrations
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ ├── fixtures
│ │ │ │ │ │ │ ├── generic.ts
│ │ │ │ │ │ │ ├── goose.ts
│ │ │ │ │ │ │ └── vscode.ts
│ │ │ │ │ │ ├── generic.spec.ts
│ │ │ │ │ │ ├── goose.spec.ts
│ │ │ │ │ │ └── vscode.spec.ts
│ │ │ │ │ ├── cursor.ts
│ │ │ │ │ ├── generic.ts
│ │ │ │ │ ├── goose.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── utils
│ │ │ │ │ │ ├── __tests__
│ │ │ │ │ │ │ ├── find-matching-install-method.spec.ts
│ │ │ │ │ │ │ ├── generate-transport.spec.ts
│ │ │ │ │ │ │ └── uwrap-matching-install-method.spec.ts
│ │ │ │ │ │ ├── find-existing-server.ts
│ │ │ │ │ │ ├── find-matching-install-method.ts
│ │ │ │ │ │ ├── generate-definition-workspace-path.ts
│ │ │ │ │ │ ├── generate-server-name.ts
│ │ │ │ │ │ ├── generate-transport.ts
│ │ │ │ │ │ ├── guards.ts
│ │ │ │ │ │ ├── parse-generic-server.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── uwrap-matching-install-method.ts
│ │ │ │ │ └── vscode.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── uninstall.ts
│ │ │ ├── mcp-utils
│ │ │ │ ├── index.ts
│ │ │ │ └── infer-target-type.ts
│ │ │ ├── observable
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── array.spec.ts
│ │ │ │ │ └── ref.spec.ts
│ │ │ │ ├── array.ts
│ │ │ │ ├── hooks
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── ref.ts
│ │ │ │ └── types.ts
│ │ │ ├── openapi
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── fixtures
│ │ │ │ │ │ ├── petstore.json
│ │ │ │ │ │ ├── slack.json
│ │ │ │ │ │ └── weather-gov.json
│ │ │ │ │ └── list-tools.spec.ts
│ │ │ │ ├── cli.ts
│ │ │ │ ├── cli
│ │ │ │ │ ├── security.ts
│ │ │ │ │ └── server-url.ts
│ │ │ │ ├── http-spec.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── list-tools.ts
│ │ │ │ ├── load-document-as-service.ts
│ │ │ │ ├── security
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ ├── fixtures
│ │ │ │ │ │ │ ├── api-key-security-fragment.ts
│ │ │ │ │ │ │ ├── empty-security-array-fragment.ts
│ │ │ │ │ │ │ ├── empty-security-fragment.ts
│ │ │ │ │ │ │ ├── http-basic-security-fragment.ts
│ │ │ │ │ │ │ ├── http-bearer-security-fragment.ts
│ │ │ │ │ │ │ ├── http-bearer-with-format-security-fragment.ts
│ │ │ │ │ │ │ ├── http-unsupported-scheme-fragment.ts
│ │ │ │ │ │ │ ├── multiple-and-security-fragment.ts
│ │ │ │ │ │ │ ├── multiple-or-security-fragment.ts
│ │ │ │ │ │ │ ├── mutual-tls-security-fragment.ts
│ │ │ │ │ │ │ ├── no-global-security-fragment.ts
│ │ │ │ │ │ │ ├── oauth2-security-fragment.ts
│ │ │ │ │ │ │ └── openid-connect-security-fragment.ts
│ │ │ │ │ │ └── resolve-security-schemes.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── resolve-security-schemes.ts
│ │ │ │ └── server
│ │ │ │ │ ├── __tests__
│ │ │ │ │ ├── apply-url-variables.spec.ts
│ │ │ │ │ ├── fixtures
│ │ │ │ │ │ ├── empty-servers-array-fragment.ts
│ │ │ │ │ │ ├── empty-servers-fragment.ts
│ │ │ │ │ │ ├── invalid-templated-server-fragment.ts
│ │ │ │ │ │ ├── invalid-url-server-fragment.ts
│ │ │ │ │ │ ├── mixed-servers-fragment.ts
│ │ │ │ │ │ ├── multiple-variables-fragment.ts
│ │ │ │ │ │ ├── non-templated-servers-fragment.ts
│ │ │ │ │ │ ├── openapi-document.ts
│ │ │ │ │ │ ├── server-with-name-fragment.ts
│ │ │ │ │ │ ├── templated-server-with-default-fragment.ts
│ │ │ │ │ │ └── templated-server-with-enum-fragment.ts
│ │ │ │ │ └── resolve-servers.spec.ts
│ │ │ │ │ ├── apply-url-variables.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── resolve-servers.ts
│ │ │ │ │ └── resolve-templated-server.ts
│ │ │ ├── platform
│ │ │ │ ├── constants.ts
│ │ │ │ ├── get-platform.ts
│ │ │ │ └── index.ts
│ │ │ ├── remix
│ │ │ │ ├── config
│ │ │ │ │ ├── __tests__
│ │ │ │ │ │ ├── fixtures
│ │ │ │ │ │ │ ├── config-with-absolute-path.json
│ │ │ │ │ │ │ ├── config-with-env.json
│ │ │ │ │ │ │ ├── config-with-file-url.json
│ │ │ │ │ │ │ ├── config-with-undefined-env.json
│ │ │ │ │ │ │ ├── config-with-url.json
│ │ │ │ │ │ │ ├── remote-config.json
│ │ │ │ │ │ │ └── valid-config.json
│ │ │ │ │ │ ├── load.spec.ts
│ │ │ │ │ │ └── schemas.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── load.ts
│ │ │ │ │ ├── parse.ts
│ │ │ │ │ └── schemas.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── manager
│ │ │ │ │ ├── client-servers
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── register-client-servers.ts
│ │ │ │ │ │ └── scoped-client-server.ts
│ │ │ │ │ ├── remix-manager.ts
│ │ │ │ │ └── servers
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── interpolate-openapi-client-config.ts
│ │ │ │ │ │ ├── register-servers.ts
│ │ │ │ │ │ └── to-transport-config.ts
│ │ │ │ ├── server.ts
│ │ │ │ └── utils
│ │ │ │ │ ├── __tests__
│ │ │ │ │ └── strict-replace-variables.spec.ts
│ │ │ │ │ ├── strict-replace-variables.ts
│ │ │ │ │ └── tools.ts
│ │ │ └── string-utils
│ │ │ │ └── index.ts
│ │ ├── register.ts
│ │ ├── rpc
│ │ │ ├── agents.ts
│ │ │ ├── base.ts
│ │ │ ├── index.ts
│ │ │ ├── mcp-servers.ts
│ │ │ └── schemas.ts
│ │ └── ui
│ │ │ ├── app.tsx
│ │ │ ├── components
│ │ │ ├── input-label.tsx
│ │ │ └── text-row.tsx
│ │ │ ├── index.ts
│ │ │ ├── input
│ │ │ ├── confirm.tsx
│ │ │ ├── index.ts
│ │ │ ├── selects
│ │ │ │ ├── components
│ │ │ │ │ ├── option-item.tsx
│ │ │ │ │ ├── options-list.tsx
│ │ │ │ │ └── search-display.tsx
│ │ │ │ ├── hooks
│ │ │ │ │ ├── useFilteredOptions.ts
│ │ │ │ │ ├── useHighlightedIndex.ts
│ │ │ │ │ ├── useMultiSelectInput.ts
│ │ │ │ │ ├── useSearchInput.ts
│ │ │ │ │ ├── useSelectInput.ts
│ │ │ │ │ └── useSelectInputBase.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── multi-select.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ └── types.ts
│ │ │ └── text.tsx
│ │ │ ├── logs.tsx
│ │ │ └── prompt.tsx
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── vitest.config.ts
├── manager
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── client-servers.ts
│ │ ├── index.ts
│ │ ├── manager.ts
│ │ ├── servers.ts
│ │ ├── storage
│ │ │ ├── index.ts
│ │ │ └── memory.ts
│ │ ├── transport.ts
│ │ └── types.ts
│ ├── tests
│ │ ├── client-servers.spec.ts
│ │ ├── manager.test.ts
│ │ └── servers.spec.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── openapi
│ ├── .turbo
│ │ ├── turbo-build.log
│ │ ├── turbo-lint.log
│ │ ├── turbo-test.log
│ │ └── turbo-typecheck.log
│ ├── CHANGELOG.md
│ ├── __tests__
│ │ ├── __fixtures__
│ │ │ └── openapi
│ │ │ │ ├── petstore.json
│ │ │ │ ├── slack.json
│ │ │ │ └── weather-gov.json
│ │ ├── __snapshots__
│ │ │ └── openapi
│ │ │ │ ├── petstore.json.snap
│ │ │ │ ├── slack.json.snap
│ │ │ │ └── weather-gov.json.snap
│ │ ├── client.test.ts
│ │ └── index.test.ts
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── client.ts
│ │ ├── index.ts
│ │ └── openapi-to-mcp.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── schemas
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ └── mcp.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── server
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── resources.ts
│ │ ├── server.ts
│ │ └── tools.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
└── utils
│ ├── CHANGELOG.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ ├── auto-trim.ts
│ ├── documents
│ │ ├── __tests__
│ │ │ ├── fixtures
│ │ │ │ ├── invalid-document.json
│ │ │ │ ├── invalid-document.yaml
│ │ │ │ ├── non-object-document.json
│ │ │ │ ├── unsupported-document.txt
│ │ │ │ ├── valid-document.json
│ │ │ │ ├── valid-document.jsonc
│ │ │ │ ├── valid-document.yaml
│ │ │ │ └── valid-document.yml
│ │ │ ├── parse.spec.ts
│ │ │ └── read.spec.ts
│ │ ├── index.ts
│ │ ├── load.ts
│ │ ├── parse.ts
│ │ ├── read.ts
│ │ └── serialize.ts
│ ├── errors.ts
│ ├── index.ts
│ └── replace-variables.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── scripts
└── openmcp-dev.sh
├── yarn.config.cjs
└── yarn.lock
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@changesets/config/schema.json",
3 | "access": "public",
4 | "baseBranch": "main",
5 | "changelog": ["@changesets/changelog-github", { "repo": "getdatanaut/openmcp" }],
6 | "commit": false,
7 | "fixed": [],
8 | "ignore": ["web"],
9 | "linked": [],
10 | "updateInternalDependencies": "patch"
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/worker-configuration.d.ts
2 | **/routeTree.gen.ts
3 | **/schema.gen.ts
4 | .yalc
5 |
--------------------------------------------------------------------------------
/.github/actions/branch-info/action.yml:
--------------------------------------------------------------------------------
1 | name: Branch Info
2 |
3 | description: 'Retrieves and normalizes information about the current branch'
4 |
5 | # Export the branch names as output to be able to use it in other jobs
6 | outputs:
7 | base-branch-name:
8 | description: The base branch name.
9 | value: ${{ steps.get-base-branch-name.outputs.branch }}
10 | branch-name:
11 | description: The current branch name.
12 | value: ${{ steps.branch-name.outputs.current_branch }}
13 |
14 | runs:
15 | using: composite
16 | steps:
17 | - name: Get branch name
18 | id: branch-name
19 | uses: tj-actions/branch-names@v8.2.1
20 |
21 | - name: Get base branch name
22 | id: get-base-branch-name
23 | shell: bash
24 | run: |
25 | if [[ "${{steps.branch-name.outputs.base_ref_branch}}" != "" ]]; then
26 | echo "branch=${{steps.branch-name.outputs.base_ref_branch}}" >> $GITHUB_OUTPUT
27 | else
28 | echo "branch=${{steps.branch-name.outputs.current_branch}}" >> $GITHUB_OUTPUT
29 | fi
30 |
--------------------------------------------------------------------------------
/.github/actions/local-payloads/README.md:
--------------------------------------------------------------------------------
1 | For use with the VSCode GitHub Local Actions extension.
2 |
--------------------------------------------------------------------------------
/.github/actions/local-payloads/example-pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "pull_request": {
3 | "head": {
4 | "ref": "mbm/ci-setup"
5 | },
6 | "base": {
7 | "ref": "main"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup
2 |
3 | description: 'Sets up the environment for a job during CI workflow'
4 |
5 | runs:
6 | using: composite
7 | steps:
8 | - name: Setup node
9 | uses: actions/setup-node@v4
10 | with:
11 | node-version: 22
12 |
13 | - name: ♻️ Restore node_modules
14 | id: yarn-nm-cache
15 | uses: actions/cache/restore@v4
16 | with:
17 | path: ./**/node_modules
18 | key: yarn-nm-cache-${{ runner.os }}-${{ hashFiles('./yarn.lock', './.yarnrc.yml') }}
19 | fail-on-cache-miss: true
20 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - action-testing
8 |
9 | permissions:
10 | contents: read
11 | actions: read
12 | pull-requests: read
13 |
14 | # Only run one deploy workflow at a time
15 | concurrency: production
16 |
17 | env:
18 | NX_CLOUD_ACCESS_TOKEN: ${{secrets.NX_CLOUD_ACCESS_TOKEN}}
19 |
20 | defaults:
21 | run:
22 | shell: bash
23 |
24 | jobs:
25 | ci-setup:
26 | uses: ./.github/workflows/ci-setup.yml
27 | secrets: inherit
28 |
29 | ci-checks:
30 | uses: ./.github/workflows/ci-checks.yml
31 | needs: [ci-setup]
32 | secrets: inherit
33 | with:
34 | typecheck-projects: ${{ needs.ci-setup.outputs.typecheck-projects }}
35 | lint-projects: ${{ needs.ci-setup.outputs.lint-projects }}
36 | test-projects: ${{ needs.ci-setup.outputs.test-projects }}
37 | build-projects: ${{ needs.ci-setup.outputs.build-projects }}
38 |
39 | release:
40 | runs-on: ubuntu-latest
41 | needs: [ci-setup, ci-checks]
42 | # if: needs.ci-setup.outputs.build-projects
43 | permissions:
44 | pull-requests: write
45 | contents: write
46 | steps:
47 | - name: Checkout
48 | uses: actions/checkout@v4
49 |
50 | - name: Setup
51 | uses: './.github/actions/setup'
52 |
53 | - name: Build
54 | run: yarn build -p openmcp "@openmcp/*"
55 |
56 | - name: Set NPM Auth Token
57 | run: yarn config set npmAuthToken "$NPM_TOKEN"
58 | env:
59 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
60 |
61 | - name: Create Release Pull Request or Publish to NPM
62 | id: changesets
63 | uses: changesets/action@v1
64 | with:
65 | publish: yarn changeset publish
66 | commit: 'ci: release packages'
67 | title: 'ci: release packages'
68 | env:
69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: PR
2 |
3 | on:
4 | pull_request:
5 |
6 | permissions:
7 | contents: read
8 | actions: read
9 | pull-requests: read
10 |
11 | concurrency:
12 | group: ${{ github.ref_name }}
13 | # Here we specify that we'll cancel any "in progress" workflow of the same group. Thus if we push, ammend a commit and push
14 | # again the previous workflow will be cancelled. This saves us github action build minutes and avoid any conflicts
15 | cancel-in-progress: true
16 |
17 | env:
18 | NX_CLOUD_ACCESS_TOKEN: ${{secrets.NX_CLOUD_ACCESS_TOKEN}}
19 |
20 | defaults:
21 | run:
22 | shell: bash
23 |
24 | jobs:
25 | ci-setup:
26 | uses: ./.github/workflows/ci-setup.yml
27 | secrets: inherit
28 |
29 | ci-checks:
30 | uses: ./.github/workflows/ci-checks.yml
31 | needs: [ci-setup]
32 | secrets: inherit
33 | with:
34 | typecheck-projects: ${{ needs.ci-setup.outputs.typecheck-projects }}
35 | lint-projects: ${{ needs.ci-setup.outputs.lint-projects }}
36 | test-projects: ${{ needs.ci-setup.outputs.test-projects }}
37 | build-projects: ${{ needs.ci-setup.outputs.build-projects }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | dist
5 | build
6 | # do not ignore built dist files in yalc'd packages
7 | !.yalc/**/dist
8 | tmp
9 | out-tsc
10 | .cache
11 | .wrangler
12 | storybook-static
13 | .nx
14 | stats.html
15 | .vinxi
16 | .vercel
17 | .output
18 |
19 | # typescript
20 | *.tsbuildinfo
21 |
22 | # dependencies
23 | node_modules
24 |
25 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
26 | .pnp.*
27 | .yarn/*
28 | !.yarn/patches
29 | !.yarn/plugins
30 | !.yarn/releases
31 | !.yarn/sdks
32 | !.yarn/versions
33 |
34 | # IDEs and editors
35 | /.idea
36 | .project
37 | .classpath
38 | .c9/
39 | *.launch
40 | .settings/
41 | *.sublime-workspace
42 |
43 | # IDE - VSCode
44 | .vscode/*
45 | !.vscode/settings.json
46 | !.vscode/tasks.json
47 | !.vscode/launch.json
48 | !.vscode/extensions.json
49 |
50 | # misc
51 | /coverage
52 | yarn-error.log
53 | .no-checkin
54 | .ignore
55 |
56 | # System Files
57 | .DS_Store
58 | Thumbs.db
59 |
60 | # env
61 | .env*
62 | !.env.example
63 | .dev.vars
64 |
65 | *storybook.log
66 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "typescript.tsdk": "node_modules/typescript/lib",
4 | "typescript.preferences.importModuleSpecifierEnding": "js",
5 | "eslint.runtime": "node",
6 | "files.associations": {
7 | "wrangler.json": "jsonc",
8 | "*.css": "tailwindcss"
9 | },
10 | "yaml.schemas": {
11 | "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
12 | },
13 | "editor.quickSuggestions": {
14 | "strings": true
15 | },
16 | "tailwindCSS.rootFontSize": 14,
17 | "tailwindCSS.experimental.classRegex": [
18 | "tw`([^`]*)`",
19 | ["(?:tx|cn|twMerge|tn)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"]
20 | ],
21 | "vitest.disableWorkspaceWarning": true,
22 | "[typescript]": {
23 | "editor.defaultFormatter": "esbenp.prettier-vscode"
24 | },
25 | "[typescriptreact]": {
26 | "editor.defaultFormatter": "esbenp.prettier-vscode"
27 | },
28 | "[jsonc]": {
29 | "editor.defaultFormatter": "esbenp.prettier-vscode"
30 | },
31 | "editor.codeActionsOnSave": {
32 | "source.fixAll": "explicit"
33 | },
34 | "files.readonlyInclude": {
35 | "**/routeTree.gen.ts": true
36 | },
37 | "files.watcherExclude": {
38 | "**/routeTree.gen.ts": true
39 | },
40 | "search.exclude": {
41 | "**/routeTree.gen.ts": true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableConstraintsChecks: true
2 |
3 | nodeLinker: node-modules
4 |
5 | plugins:
6 | - checksum: 8f13acff9aef76f5fbfb5474b6d23b14ed3f49258cf4e344229e3515e42f4d6990e3c51b9ab176aa46c407fb9ca97fc0902c6400db5a44e9994d0b53512f3aed
7 | path: .yarn/plugins/@yarnpkg/plugin-engines.cjs
8 | spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js"
9 |
10 | yarnPath: .yarn/releases/yarn-4.9.1.cjs
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 1. Clone the repo
4 | 2. `yarn`
5 | 3. Make changes
6 | 4. Make sure `yarn test`, `yarn lint`, `yarn typecheck`, and `yarn build` pass
7 | 5. If your changes should result in a new published version of one or more packages, run `yarn changeset`
8 | 6. Open a PR
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2025-present Datanaut Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation the
5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
6 | persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
9 | Software.
10 |
11 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | packages/cli/README.md
--------------------------------------------------------------------------------
/apps/web/app.config.ts:
--------------------------------------------------------------------------------
1 | import tailwindcss from '@tailwindcss/vite';
2 | import { defineConfig } from '@tanstack/react-start/config';
3 | import { cloudflare } from 'unenv';
4 | import { analyzer } from 'vite-bundle-analyzer';
5 | import tsConfigPaths from 'vite-tsconfig-paths';
6 |
7 | // flip to true if you want to analyze the bundle.. doesn't work well w vinxi though
8 | const ANALYZE = process.argv.includes('--analyze') || false;
9 |
10 | export default defineConfig({
11 | server: {
12 | compatibilityDate: '2025-04-30',
13 | preset: 'cloudflare_module',
14 | unenv: cloudflare,
15 | },
16 | tsr: {
17 | routeToken: 'layout',
18 | appDirectory: 'src',
19 | },
20 | vite: {
21 | plugins: [tsConfigPaths(), tailwindcss(), ANALYZE ? analyzer() : null],
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { reactConfig } from '@datanaut/eslint-config/react';
2 | import pluginRouter from '@tanstack/eslint-plugin-router';
3 |
4 | export default [...reactConfig, ...pluginRouter.configs['flat/recommended']];
5 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.0.0",
4 | "private": true,
5 | "sideEffects": false,
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vinxi dev",
9 | "build": "vinxi build",
10 | "build.analyze": "vinxi build -- --analyze",
11 | "preview": "wrangler --cwd .output dev",
12 | "deploy": "wrangler --cwd .output deploy"
13 | },
14 | "dependencies": {
15 | "@datanaut/ui-primitives": "0.1.4",
16 | "@fortawesome/free-brands-svg-icons": "6.7.2",
17 | "@fortawesome/free-regular-svg-icons": "6.7.2",
18 | "@fortawesome/free-solid-svg-icons": "6.7.2",
19 | "@tanstack/react-router": "1.119.0",
20 | "@tanstack/react-start": "1.119.2",
21 | "react": "19.1.0",
22 | "react-dom": "19.1.0",
23 | "vinxi": "0.5.6",
24 | "zod": "^3.24.3"
25 | },
26 | "devDependencies": {
27 | "@datanaut/eslint-config": "0.1.1",
28 | "@datanaut/tsconfig": "0.1.3",
29 | "@tailwindcss/vite": "4.1.5",
30 | "@tanstack/eslint-plugin-router": "1.115.0",
31 | "@types/react": "19.1.3",
32 | "@types/react-dom": "19.1.3",
33 | "tailwindcss": "4.1.5",
34 | "unenv": "1.10.0",
35 | "vite": "6.3.5",
36 | "vite-bundle-analyzer": "0.20.1",
37 | "vite-tsconfig-paths": "5.1.4",
38 | "wrangler": "4.14.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getdatanaut/openmcp/fa8927ba55489bf73ab5ab0114444234cccfac48/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/src/assets/app.css:
--------------------------------------------------------------------------------
1 | @import '@datanaut/ui-primitives/styles';
2 |
3 | /* https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources */
4 | @source "../../../../node_modules/@datanaut/ui-primitives/src";
5 |
6 | @font-face {
7 | src: url(./fonts/ml/0-normal.woff2) format('woff2');
8 | font-family: ml;
9 | font-weight: 100 900;
10 | font-style: normal;
11 | unicode-range: U+0020-007F;
12 | }
13 |
14 | @font-face {
15 | src: url(./fonts/ml/1-italic.woff2) format('woff2');
16 | font-family: ml;
17 | font-weight: 100 900;
18 | font-style: italic;
19 | unicode-range: U+0020-007F;
20 | }
21 |
22 | @theme {
23 | --font-mono: ml, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
24 | }
25 |
26 | :root {
27 | --root-font-size: 14px;
28 | }
29 |
30 | @layer base {
31 | html,
32 | body {
33 | @apply min-h-dvh;
34 |
35 | font-family: var(--font-mono);
36 | letter-spacing: -0.02em;
37 | }
38 |
39 | html {
40 | color-scheme: light dark;
41 | font-size: var(--root-font-size);
42 | }
43 |
44 | body {
45 | @apply ak-layer-canvas;
46 | }
47 |
48 | * {
49 | @apply edge-d selection:ak-layer-mix-primary/40;
50 |
51 | /* to prevent grid and flex items from spilling out of their container */
52 | min-width: 0;
53 | }
54 |
55 | a {
56 | @apply focus-visible:ak-outline focus-visible:outline-[1.5px] focus-visible:outline-offset-[1.5px];
57 | }
58 |
59 | h1,
60 | h2,
61 | h3,
62 | h4 {
63 | /* balance headings across multiple lines into an even block */
64 | text-wrap: balance;
65 | }
66 |
67 | p {
68 | /* prevent text orphans (single words on last line) */
69 | text-wrap: pretty;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/apps/web/src/assets/fonts/ml/0-normal.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getdatanaut/openmcp/fa8927ba55489bf73ab5ab0114444234cccfac48/apps/web/src/assets/fonts/ml/0-normal.woff2
--------------------------------------------------------------------------------
/apps/web/src/assets/fonts/ml/1-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getdatanaut/openmcp/fa8927ba55489bf73ab5ab0114444234cccfac48/apps/web/src/assets/fonts/ml/1-italic.woff2
--------------------------------------------------------------------------------
/apps/web/src/client.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import { StartClient } from '@tanstack/react-start';
3 | import { hydrateRoot } from 'react-dom/client';
4 |
5 | import { createRouter } from './router.tsx';
6 |
7 | const router = createRouter();
8 |
9 | hydrateRoot(document, );
10 |
--------------------------------------------------------------------------------
/apps/web/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter as createTanStackRouter } from '@tanstack/react-router';
2 |
3 | import { routeTree } from './routeTree.gen.ts';
4 |
5 | export function createRouter() {
6 | return createTanStackRouter({
7 | scrollRestoration: true,
8 | scrollRestorationBehavior: 'auto',
9 | routeTree,
10 | defaultPreload: 'intent',
11 | });
12 | }
13 |
14 | declare module '@tanstack/react-router' {
15 | interface Register {
16 | router: ReturnType;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from '@tanstack/react-router';
2 |
3 | import appCss from '~/assets/app.css?url';
4 |
5 | export const Route = createRootRouteWithContext<{}>()({
6 | component: RootComponent,
7 | head: () => ({
8 | meta: [
9 | {
10 | charSet: 'utf-8',
11 | },
12 | {
13 | name: 'viewport',
14 | content: 'width=device-width, initial-scale=1',
15 | },
16 | {
17 | title: 'OpenMCP',
18 | },
19 | {
20 | name: 'description',
21 | content: 'Instant MCP servers from any OpenAPI file.',
22 | },
23 | ],
24 |
25 | links: [
26 | { rel: 'stylesheet', href: appCss },
27 | { rel: 'icon', href: '/favicon.ico' },
28 | ],
29 | }),
30 | });
31 |
32 | function RootComponent() {
33 | return (
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | function RootDocument({ children }: { children: React.ReactNode }) {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 | {children}
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/src/ssr.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import { getRouterManifest } from '@tanstack/react-start/router-manifest';
3 | import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server';
4 |
5 | import { createRouter } from './router.tsx';
6 |
7 | // eslint-disable-next-line react-refresh/only-export-components
8 | export default createStartHandler({
9 | createRouter,
10 | getRouterManifest,
11 | })(defaultStreamHandler);
12 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.react.json",
3 | "include": ["src", "app.config.ts"],
4 | "compilerOptions": {
5 | "rootDir": ".",
6 | "types": ["vite/client"],
7 | "paths": {
8 | "~/*": ["./src/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "../../node_modules/wrangler/config-schema.json",
7 | "name": "openmcp-web",
8 | "main": "./.output/server/index.mjs",
9 | "compatibility_date": "2025-04-08",
10 | "compatibility_flags": ["nodejs_compat"],
11 | "assets": {
12 | "html_handling": "drop-trailing-slash",
13 | "directory": "./.output/public/",
14 | "binding": "ASSETS",
15 | },
16 | "routes": [
17 | {
18 | "pattern": "openmcp.datanaut.ai",
19 | "custom_domain": true,
20 | },
21 | ],
22 | }
23 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
3 | "defaultBase": "main",
4 | "parallel": 7,
5 | "nxCloudId": "68191ad677166d4da489ea72",
6 | "cli": {
7 | "packageManager": "yarn"
8 | },
9 | "namedInputs": {
10 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
11 | "production": [
12 | "default",
13 | "!{projectRoot}/**/__tests__/**",
14 | "!{projectRoot}/eslint.config.js",
15 | "!{projectRoot}/**/README.md"
16 | ],
17 | "sharedGlobals": []
18 | },
19 | "targetDefaults": {
20 | "build": {
21 | "dependsOn": ["^build"],
22 | "inputs": ["production", "^production"],
23 | "cache": true
24 | },
25 | "test": {
26 | "inputs": ["default", "^production"],
27 | "cache": true
28 | },
29 | "typecheck": {
30 | "cache": true
31 | },
32 | "lint": {
33 | "cache": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/cli/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node --no-warnings
2 | import process from 'node:process';
3 |
4 | import register from '#register';
5 |
6 | await register(process.argv.slice(2));
7 |
--------------------------------------------------------------------------------
/packages/cli/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default [
4 | ...baseConfig,
5 |
6 | {
7 | files: ['src/**/*.{ts,tsx}'],
8 | rules: {
9 | 'no-console': 'error',
10 | 'import/extensions': 'off', // not needed if moduleResolution is set to node16
11 | 'import/no-extraneous-dependencies': 'error',
12 | },
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/packages/cli/src/api/index.ts:
--------------------------------------------------------------------------------
1 | // API exposed by CLI
2 | // must be environment independent - the members exported here must not use Node.js specific APIs
3 | export { getInstallHints, type IntegrationName } from '../libs/mcp-clients/index.isomorphic.ts';
4 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/install/handler.ts:
--------------------------------------------------------------------------------
1 | import console from '#libs/console';
2 | import { install, type IntegrationName } from '#libs/mcp-clients';
3 |
4 | import { getAgentById } from '../../libs/datanaut/agent.ts';
5 | import { inferTargetType } from '../../libs/mcp-utils/index.ts';
6 | import createOpenAPIRemix from './openapi/index.ts';
7 |
8 | type Flags = {
9 | client: IntegrationName;
10 | type: 'agent-id' | 'openapi' | undefined;
11 | scope: 'global' | 'local' | 'prefer-local';
12 | };
13 |
14 | export default async function handler(target: string, { type, client, scope }: Flags) {
15 | const server = await getServer(target, type ?? (await inferTargetType(target)), client, scope);
16 | const ctx = {
17 | cwd: process.cwd(),
18 | logger: console,
19 | } as const;
20 | await install(ctx, client, server, scope);
21 | }
22 |
23 | async function getServer(
24 | target: string,
25 | type: NonNullable,
26 | client: IntegrationName,
27 | scope: Flags['scope'],
28 | ) {
29 | switch (type) {
30 | case 'agent-id': {
31 | const agent = await getAgentById(target);
32 | return {
33 | id: agent.id,
34 | name: agent.name,
35 | target: agent.id,
36 | } as const;
37 | }
38 | case 'openapi':
39 | return createOpenAPIRemix(target, client, scope);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/install/openapi/create-remix.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs/promises';
2 | import * as path from 'node:path';
3 |
4 | import console from '#libs/console';
5 | import type { Config as RemixDefinition } from '#libs/remix';
6 |
7 | export default async function createRemix(cwd: string, filepath: string, remix: RemixDefinition): Promise {
8 | const dirname = path.dirname(filepath);
9 | if (dirname !== cwd) {
10 | try {
11 | await fs.mkdir(dirname, { recursive: true });
12 | } catch (error) {
13 | throw new Error(formatErrorMessage(`Failed to create directory at ${JSON.stringify(dirname)}`, error));
14 | }
15 | }
16 |
17 | const strFilepath = JSON.stringify(filepath);
18 |
19 | try {
20 | await fs.writeFile(filepath, JSON.stringify(remix, null, 2));
21 | console.success(`Created OpenAPI openmcp definition at ${strFilepath}`);
22 | } catch (error) {
23 | throw new Error(formatErrorMessage(`Failed to write file at ${strFilepath}`, error));
24 | }
25 | }
26 |
27 | function formatErrorMessage(message: string, error: unknown): string {
28 | const errorMessage = error instanceof Error ? error.message : null;
29 | return `${errorMessage ? `${message}: ${errorMessage}` : message}`;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/install/openapi/index.ts:
--------------------------------------------------------------------------------
1 | import { OperationCanceledError } from '#errors';
2 | import console from '#libs/console';
3 | import type { InstallLocation, IntegrationName, Remix } from '#libs/mcp-clients';
4 |
5 | import createRemix from './create-remix.ts';
6 | import generateRemixDefinition from './generate-remix.ts';
7 | import negotiateFilepath from './negotiate-filepath.ts';
8 |
9 | export default async function createOpenAPIRemix(
10 | openapiLocation: string,
11 | client: IntegrationName,
12 | installLocation: InstallLocation,
13 | ): Promise {
14 | const cwd = process.cwd();
15 | console.start('Generating OpenAPI openmcp definition...');
16 | try {
17 | const remix = await negotiateFilepath(cwd, client, installLocation);
18 | const definition = await generateRemixDefinition(remix, openapiLocation);
19 | await createRemix(cwd, remix.filepath, definition);
20 |
21 | return {
22 | name: 'openmcp',
23 | target: remix.filepath,
24 | };
25 | } catch (error) {
26 | if (error instanceof OperationCanceledError) {
27 | throw error;
28 | } else {
29 | throw new Error(
30 | `Failed to generate OpenAPI openmcp definition: ${error instanceof Error ? error.message : error}`,
31 | );
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/install/openapi/types.ts:
--------------------------------------------------------------------------------
1 | export type Context = {
2 | cwd: string;
3 | console: Console;
4 | };
5 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/login/index.ts:
--------------------------------------------------------------------------------
1 | import type { CommandModule } from 'yargs';
2 |
3 | import { createHandler } from '#libs/cli-utils';
4 | import { login } from '#libs/datanaut-auth-cli';
5 |
6 | export default {
7 | command: 'login',
8 | describe: process.env['NODE_ENV'] === 'development' ? 'Login to the CLI' : false,
9 | handler: createHandler(login, true),
10 | } satisfies CommandModule;
11 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/logout/index.ts:
--------------------------------------------------------------------------------
1 | import type { CommandModule } from 'yargs';
2 |
3 | import { createHandler } from '#libs/cli-utils';
4 | import { logout } from '#libs/datanaut-auth-cli';
5 |
6 | export default {
7 | command: 'logout',
8 | describe: process.env['NODE_ENV'] === 'development' ? 'Logout from the CLI' : false,
9 | handler: createHandler(logout, false),
10 | } satisfies CommandModule;
11 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/run/index.ts:
--------------------------------------------------------------------------------
1 | import { type Argv } from 'yargs';
2 |
3 | import { createHandler } from '#libs/cli-utils';
4 |
5 | const builder = (yargs: Argv) =>
6 | yargs.strict().options({
7 | server: {
8 | type: 'string',
9 | describe: 'The name or id of the server to start',
10 | conflicts: 'config',
11 | },
12 | secret: {
13 | type: 'string',
14 | describe: 'The secret to use for authentication',
15 | implies: 'server',
16 | },
17 | config: {
18 | type: 'string',
19 | describe: 'The filepath to the local config file',
20 | conflicts: 'server',
21 | },
22 | } as const);
23 |
24 | export default {
25 | describe: 'Start a new server',
26 | command: 'run',
27 | builder,
28 | handler: createHandler(async args => {
29 | const { default: handler } = await import('./handler.ts');
30 | const { server, secret, config } = args as Awaited['argv']>;
31 | if (typeof config === 'string') {
32 | await handler({
33 | configFile: config,
34 | });
35 | } else {
36 | await handler({
37 | server: String(server),
38 | secret,
39 | });
40 | }
41 | }, false),
42 | };
43 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/uninstall/handler.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs/promises';
2 |
3 | import { loadDocument } from '@openmcp/utils/documents';
4 |
5 | import console from '#libs/console';
6 | import { type IntegrationName, uninstall } from '#libs/mcp-clients';
7 |
8 | import { getAgentById } from '../../libs/datanaut/agent.ts';
9 | import { inferTargetType } from '../../libs/mcp-utils/index.ts';
10 |
11 | type Flags = {
12 | client: IntegrationName;
13 | type: 'agent-id' | 'openapi' | undefined;
14 | scope: 'global' | 'local' | 'prefer-local';
15 | };
16 |
17 | export default async function handler(target: string, { type, client, scope }: Flags) {
18 | const server = await getServer(target, type ?? (await inferTargetType(target)));
19 | const ctx = {
20 | cwd: process.cwd(),
21 | logger: console,
22 | } as const;
23 | await uninstall(ctx, client, server, scope);
24 | }
25 |
26 | async function getServer(target: string, type: NonNullable) {
27 | switch (type) {
28 | case 'agent-id': {
29 | const agent = await getAgentById(target);
30 | return {
31 | id: agent.id,
32 | name: agent.name,
33 | target: agent.id,
34 | } as const;
35 | }
36 | case 'openapi': {
37 | const name = (
38 | await loadDocument(
39 | {
40 | fs,
41 | fetch,
42 | },
43 | target,
44 | )
45 | )['info']?.['title'];
46 | return {
47 | id: target,
48 | name: String(name ?? target),
49 | target: target,
50 | };
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/uninstall/index.ts:
--------------------------------------------------------------------------------
1 | import type { CommandModule } from 'yargs';
2 |
3 | import { createHandler } from '#libs/cli-utils';
4 |
5 | import type { BuilderArgv } from '../install/index.ts';
6 | import { builder } from '../install/index.ts';
7 | import handler from './handler.ts';
8 |
9 | export default {
10 | describe: process.env['NODE_ENV'] === 'development' ? 'Uninstall the target' : false,
11 | command: 'uninstall ',
12 | builder,
13 | handler: createHandler(async args => {
14 | const { target, client, type, scope } = args as BuilderArgv & {
15 | target: string;
16 | };
17 |
18 | await handler(target, {
19 | client,
20 | type,
21 | scope,
22 | });
23 | }, true),
24 | } satisfies CommandModule;
25 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/handler.ts:
--------------------------------------------------------------------------------
1 | import console from '#libs/console';
2 |
3 | import { rpcClient } from '../../libs/datanaut/sdk/sdk.ts';
4 | import createConnectedClient from './mcp-utils/create-client.ts';
5 | import listMcpTools from './mcp-utils/get-tools.ts';
6 | import createTransportDefinition from './transport-definitions/index.ts';
7 | import type { ServerDefinition } from './types.ts';
8 |
9 | export default async function handler(definition: ServerDefinition): Promise {
10 | const { transport, transportConfig, configSchema, externalId, metadata } =
11 | await createTransportDefinition(definition);
12 | const defaultAnnotations = {
13 | iconUrl: definition.iconUrl,
14 | developer: definition.developer,
15 | };
16 |
17 | await using client = await createConnectedClient(transport, defaultAnnotations);
18 | const serverVersion = client.getServerVersion();
19 | if (!serverVersion) {
20 | throw new Error('Failed to get server version');
21 | }
22 | const serverAnnotations = client.getServerAnnotations();
23 |
24 | const mcpServer = {
25 | name: serverAnnotations?.serverName ?? metadata?.name ?? serverVersion.name,
26 | externalId: externalId ?? serverVersion.name,
27 | description: metadata?.description,
28 | developer: definition.developer ?? metadata?.developer,
29 | developerUrl: definition.developerUrl || metadata?.developerUrl,
30 | iconUrl: definition.iconUrl ?? serverAnnotations?.iconUrl ?? metadata?.iconUrl,
31 | sourceUrl: definition.sourceUrl,
32 | configSchema,
33 | transport: transportConfig,
34 | tools: await listMcpTools(client),
35 | };
36 |
37 | console.start(`Uploading server "${mcpServer.name}"...`);
38 | await rpcClient.cli.mcpServers.upload(mcpServer);
39 | console.success(`Server "${mcpServer.name}" successfully uploaded`);
40 | }
41 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/mcp-utils/create-client.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
3 |
4 | import packageJson from '../../../../package.json' with { type: 'json' };
5 |
6 | interface ServerAnnotations {
7 | // https://github.com/modelcontextprotocol/modelcontextprotocol/blob/dfd270157380c645d0731f9f2ffbc89f75fba47d/schema/2025-03-26/schema.ts#L177
8 | serverName?: string;
9 | iconUrl?: string;
10 | [key: string]: unknown;
11 | }
12 |
13 | export type ConnectedClient = Pick<
14 | Client,
15 | 'listTools' | 'listPrompts' | 'listResources' | 'listResourceTemplates' | 'getServerVersion' | 'getServerCapabilities'
16 | > & {
17 | getServerAnnotations(): ServerAnnotations | undefined;
18 | } & AsyncDisposable;
19 |
20 | export default async function createConnectedClient(
21 | transport: Transport,
22 | annotations: Record,
23 | ): Promise {
24 | const client = new Client({
25 | name: packageJson.name,
26 | version: packageJson.version,
27 | });
28 |
29 | try {
30 | await client.connect(transport);
31 | } catch (error) {
32 | throw new Error(`Failed to connect to server: ${error}`);
33 | }
34 |
35 | return {
36 | getServerAnnotations(): ServerAnnotations | undefined {
37 | return {
38 | // will work once SDK adds support for server annotations this should work
39 | ...client['getServerAnnotations']?.(),
40 | ...annotations,
41 | };
42 | },
43 | getServerCapabilities: client.getServerCapabilities.bind(client),
44 | getServerVersion: client.getServerVersion.bind(client),
45 | listTools: client.listTools.bind(client),
46 | listPrompts: client.listPrompts.bind(client),
47 | listResources: client.listResources.bind(client),
48 | listResourceTemplates: client.listResourceTemplates.bind(client),
49 | async [Symbol.asyncDispose]() {
50 | await client.close();
51 | },
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/mcp-utils/get-tools.ts:
--------------------------------------------------------------------------------
1 | import console from '#libs/console';
2 |
3 | import type { rpcClient } from '../../../libs/datanaut/sdk/sdk.ts';
4 | import { getSummary } from '../utils/string.ts';
5 | import type { ConnectedClient } from './create-client.ts';
6 |
7 | type Tool = NonNullable[0]['tools']>[number];
8 |
9 | function unwrapBooleanOrUndefined(value: unknown) {
10 | return typeof value === 'boolean' ? value : undefined;
11 | }
12 |
13 | export default async function getTools(client: ConnectedClient): Promise {
14 | const capabilities = client.getServerCapabilities();
15 | if (!capabilities?.tools) {
16 | console.warn(`No "tools" capability found`);
17 | return [];
18 | }
19 |
20 | try {
21 | return (await client.listTools()).tools.map(tool => {
22 | const annotations = tool['annotations'] as Record | undefined;
23 | const title = annotations?.['title'];
24 | return {
25 | name: tool.name,
26 | description: tool.description,
27 | summary: tool.description ? getSummary(tool.description) : undefined,
28 | displayName: typeof title === 'string' ? title : undefined,
29 | inputSchema: tool.inputSchema,
30 | outputSchema: tool['outputSchema'] as Tool['outputSchema'] | undefined,
31 | isReadonly: unwrapBooleanOrUndefined(annotations?.['readOnlyHint']),
32 | isDestructive: unwrapBooleanOrUndefined(annotations?.['destructiveHint']),
33 | isIdempotent: unwrapBooleanOrUndefined(annotations?.['idempotentHint']),
34 | isOpenWorld: unwrapBooleanOrUndefined(annotations?.['openWorldHint']),
35 | };
36 | });
37 | } catch (error) {
38 | throw new Error(`Failed to list tools: ${error}`);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/transport-definitions/index.ts:
--------------------------------------------------------------------------------
1 | import type { ServerDefinition } from '../types.ts';
2 | import parseCmd from '../utils/parse-cmd/index.ts';
3 | import createOpenAPITransportDefinition from './openapi.ts';
4 | import createSSETransportDefinition from './sse.ts';
5 | import createStdioTransportDefinition from './stdio.ts';
6 | import type { TransportDefinition, TransportType } from './types.ts';
7 |
8 | export default async function createTransportDefinition(
9 | definition: ServerDefinition,
10 | ): Promise> {
11 | switch (definition.type) {
12 | case 'stdio':
13 | return createStdioTransportDefinition(parseCmd(definition.input), process.cwd());
14 | case 'sse':
15 | return createSSETransportDefinition(definition.url, {
16 | requestInit: {
17 | headers: new Headers(definition.headers),
18 | },
19 | });
20 | case 'streamable-http': {
21 | const { default: createStreamableHTTPTransportDefinition } = await import('./streamable-http.ts');
22 | return createStreamableHTTPTransportDefinition(definition.url, {
23 | requestInit: {
24 | headers: new Headers(definition.headers),
25 | },
26 | });
27 | }
28 | case 'openapi':
29 | return createOpenAPITransportDefinition(definition.uri, definition.serverUrl);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/transport-definitions/sse.ts:
--------------------------------------------------------------------------------
1 | import { SSEClientTransport, type SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
2 |
3 | import ConfigSchema from '../utils/config-schema.ts';
4 | import maskHeaders from '../utils/mask-headers.ts';
5 | import maskUrl from '../utils/mask-url.ts';
6 | import type { TransportDefinition } from './types.ts';
7 |
8 | export type SSETransportDefinition = TransportDefinition<'sse'>;
9 |
10 | export default function createSSETransportDefinition(
11 | url: URL,
12 | opts: SSEClientTransportOptions,
13 | ): SSETransportDefinition {
14 | const configSchema = new ConfigSchema();
15 | const maskedUrl = maskUrl(configSchema, url);
16 | const maskedHeaders = opts.requestInit?.headers
17 | ? maskHeaders(configSchema, new Headers(opts.requestInit.headers))
18 | : undefined;
19 |
20 | return {
21 | transport: new SSEClientTransport(url, opts),
22 | transportConfig: {
23 | type: 'sse',
24 | url: maskedUrl,
25 | headers: maskedHeaders,
26 | },
27 | configSchema: configSchema.serialize(),
28 | externalId: undefined,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/transport-definitions/stdio.ts:
--------------------------------------------------------------------------------
1 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2 |
3 | import type ParsedCommand from '../utils/parse-cmd/parsed-command.ts';
4 | import type { TransportDefinition } from './types.ts';
5 |
6 | export type StdioTransportDefinition = TransportDefinition<'stdio'>;
7 |
8 | export default function getStdioTransportDefinition(command: ParsedCommand, cwd: string): StdioTransportDefinition {
9 | return {
10 | transport: new StdioClientTransport(command.getStdioServerParameters(cwd)),
11 | transportConfig: command.getTransportConfig(),
12 | configSchema: command.configSchema.serialize(),
13 | externalId: command.externalId,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/transport-definitions/streamable-http.ts:
--------------------------------------------------------------------------------
1 | import {
2 | StreamableHTTPClientTransport,
3 | type StreamableHTTPClientTransportOptions,
4 | } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5 |
6 | import ConfigSchema from '../utils/config-schema.ts';
7 | import maskHeaders from '../utils/mask-headers.ts';
8 | import maskUrl from '../utils/mask-url.ts';
9 | import type { TransportDefinition } from './types.ts';
10 |
11 | export type StreamableHTTPTransportDefinition = TransportDefinition<'streamableHttp'>;
12 |
13 | export default function createStreamableHTTPTransportDefinition(
14 | url: URL,
15 | opts: StreamableHTTPClientTransportOptions,
16 | ): StreamableHTTPTransportDefinition {
17 | const configSchema = new ConfigSchema();
18 | const maskedUrl = maskUrl(configSchema, url);
19 | const maskedHeaders = opts.requestInit?.headers
20 | ? maskHeaders(configSchema, new Headers(opts.requestInit.headers))
21 | : undefined;
22 |
23 | return {
24 | transport: new StreamableHTTPClientTransport(url, opts),
25 | transportConfig: {
26 | type: 'streamable-http',
27 | url: maskedUrl,
28 | headers: maskedHeaders,
29 | },
30 | configSchema: configSchema.serialize(),
31 | externalId: undefined,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/transport-definitions/types.ts:
--------------------------------------------------------------------------------
1 | import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
2 | import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3 | import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4 | import type { OpenAPITransport, SSETransport, StdIOTransport, StreamableHTTPTransport } from '@openmcp/schemas/mcp';
5 |
6 | import type { OpenAPIClientTransport } from './openapi.ts';
7 |
8 | type Clients = {
9 | openapi: OpenAPIClientTransport;
10 | sse: SSEClientTransport;
11 | streamableHttp: StreamableHTTPClientTransport;
12 | stdio: StdioClientTransport;
13 | };
14 |
15 | export type TransportDefinition = {
16 | readonly transport: Clients[T];
17 | readonly transportConfig: TransportConfig;
18 | readonly configSchema:
19 | | {
20 | type: 'object';
21 | properties: {
22 | [key: string]: {
23 | type: 'string' | 'boolean' | 'number';
24 | };
25 | };
26 | required: string[];
27 | }
28 | | undefined;
29 | readonly externalId: string | undefined;
30 | // some of that won't be needed once we MCP supports more robust server annotations
31 | readonly metadata?: {
32 | readonly name?: string;
33 | readonly summary?: string;
34 | readonly description?: string;
35 | readonly developer?: string;
36 | readonly developerUrl?: string;
37 | readonly iconUrl?: string;
38 | };
39 | };
40 |
41 | export type TransportConfigs = {
42 | openapi: OpenAPITransport;
43 | sse: SSETransport;
44 | streamableHttp: StreamableHTTPTransport;
45 | stdio: StdIOTransport;
46 | };
47 |
48 | export type TransportType = keyof TransportConfigs;
49 |
50 | export type TransportConfig = TransportConfigs[T];
51 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/types.ts:
--------------------------------------------------------------------------------
1 | export type ServerDefinition = {
2 | iconUrl?: string;
3 | developer?: string;
4 | developerUrl?: string;
5 | sourceUrl?: string;
6 | } & (
7 | | {
8 | type: 'sse' | 'streamable-http';
9 | headers?: [string, string][];
10 | url: URL;
11 | }
12 | | {
13 | type: 'openapi';
14 | serverUrl?: string;
15 | uri: string;
16 | }
17 | | {
18 | type: 'stdio';
19 | input: string;
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/utils/config-schema.ts:
--------------------------------------------------------------------------------
1 | export type InferredType = 'string' | 'number' | 'boolean';
2 |
3 | export type SerializableConfigSchema = {
4 | type: 'object';
5 | properties: Record<
6 | string,
7 | {
8 | type: InferredType;
9 | }
10 | >;
11 | required: string[];
12 | };
13 |
14 | export default class ConfigSchema {
15 | readonly #type = 'object';
16 | readonly #properties: Record = {};
17 | #size = 0;
18 |
19 | public add(name: string, type: InferredType): string {
20 | if (!Object.hasOwn(this.#properties, name)) {
21 | this.#size++;
22 | this.#properties[name] = { type };
23 | }
24 |
25 | return name;
26 | }
27 |
28 | public get size() {
29 | return this.#size;
30 | }
31 |
32 | public inferType(value: string): InferredType {
33 | if (value === 'true' || value === 'false') {
34 | return 'boolean';
35 | }
36 |
37 | if (value === '0' || value === '1') {
38 | return 'boolean';
39 | }
40 |
41 | if (!Number.isNaN(Number.parseInt(value))) {
42 | return 'number';
43 | }
44 |
45 | return 'string';
46 | }
47 |
48 | public serialize(): SerializableConfigSchema | undefined {
49 | if (this.#size === 0) {
50 | return;
51 | }
52 |
53 | return {
54 | type: this.#type,
55 | properties: this.#properties,
56 | required: Object.keys(this.#properties),
57 | };
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/utils/parse-cmd/index.ts:
--------------------------------------------------------------------------------
1 | import ConfigSchema from '../config-schema.ts';
2 | import parseDockerRun from './parse-docker-run.ts';
3 | import parseEnvVariables from './parse-env-variables.ts';
4 | import parseGeneric from './parse-generic.ts';
5 | import parseNpx from './parse-npx-command.ts';
6 | import ParsedCommand from './parsed-command.ts';
7 |
8 | function parseCommand(input: string): string {
9 | if (input.length === 0) {
10 | throw new Error('No command name found');
11 | }
12 |
13 | const index = input.indexOf(' ');
14 | return index === -1 ? input : input.slice(0, index);
15 | }
16 |
17 | const parsers = {
18 | npx: parseNpx,
19 | docker: parseDockerRun,
20 | generic: parseGeneric,
21 | } as const;
22 |
23 | export default function parseCmd(input: string): ParsedCommand {
24 | const configSchema = new ConfigSchema();
25 | const { vars, lastIndex: offset } = parseEnvVariables(input);
26 | const env: Record = {};
27 | for (const [key, value] of vars) {
28 | configSchema.add(key, configSchema.inferType(value));
29 | env[key] = value;
30 | }
31 |
32 | const actualInput = input.slice(offset).trim();
33 | const commandName = parseCommand(actualInput);
34 | const parse = parsers[commandName] ?? parsers.generic;
35 | return new ParsedCommand({
36 | ...parse(configSchema, commandName, actualInput.slice(commandName.length + 1)),
37 | configSchema,
38 | env,
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/utils/parse-cmd/parse-generic.ts:
--------------------------------------------------------------------------------
1 | import type ConfigSchema from '../config-schema.ts';
2 | import type { Result } from './types.ts';
3 |
4 | function splitByWhitespacePreservingQuotes(input: string): string[] {
5 | // Regular expression to match:
6 | // 1. Double-quoted strings: "[^"]*"
7 | // 2. Single-quoted strings: '[^']*'
8 | // 3. Sequences of non-whitespace characters: \S+
9 | const regex = /"[^"]*"|'[^']*'|\S+/g;
10 | const matches = input.match(regex);
11 |
12 | // Return the array of matches, or an empty array if no matches are found
13 | return matches || [];
14 | }
15 |
16 | function isChar(code: number): boolean {
17 | return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
18 | }
19 |
20 | export default function parseGeneric(
21 | _configSchema: ConfigSchema,
22 | command: string,
23 | input: string,
24 | ): Omit {
25 | const args = splitByWhitespacePreservingQuotes(input);
26 | const externalId = [command];
27 | for (const arg of args) {
28 | if (isChar(arg.charCodeAt(0))) {
29 | externalId.push(arg);
30 | } else {
31 | break;
32 | }
33 | }
34 |
35 | return {
36 | command,
37 | args: args.map(arg => ({
38 | type: 'positional',
39 | dataType: 'string',
40 | value: arg,
41 | masked: null,
42 | })),
43 | externalId: externalId.join('-'),
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/utils/parse-cmd/types.ts:
--------------------------------------------------------------------------------
1 | import type ConfigSchema from '../config-schema.ts';
2 |
3 | export type ResultArg =
4 | | {
5 | type: 'command';
6 | value: string;
7 | }
8 | | {
9 | type: 'positional';
10 | dataType: 'string' | 'number';
11 | value: string;
12 | masked: string | null;
13 | }
14 | | ResultArgFlag;
15 |
16 | export type ResultArgFlag = {
17 | type: 'flag';
18 | name: string;
19 | } & (
20 | | {
21 | dataType: 'boolean';
22 | value: true | false;
23 | }
24 | | {
25 | dataType: 'string' | 'number';
26 | value: string;
27 | masked: string | null;
28 | }
29 | );
30 |
31 | export type Result = {
32 | externalId: string;
33 | command: string;
34 | args: ResultArg[];
35 | env: Record;
36 | configSchema: ConfigSchema;
37 | };
38 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/upload/utils/string.ts:
--------------------------------------------------------------------------------
1 | const SENTENCE_REGEX = /[A-Za-z][^.!?]+[.!?]/;
2 |
3 | export function getSummary(value: string): string | undefined {
4 | return SENTENCE_REGEX.exec(value)?.[0];
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/whoami/index.ts:
--------------------------------------------------------------------------------
1 | import type { CommandModule } from 'yargs';
2 |
3 | import { createHandler } from '#libs/cli-utils';
4 | import { whoami } from '#libs/datanaut-auth-cli';
5 |
6 | export default {
7 | command: 'whoami',
8 | describe: process.env['NODE_ENV'] === 'development' ? 'Display Datanaut email' : false,
9 | handler: createHandler(whoami, true),
10 | } satisfies CommandModule;
11 |
--------------------------------------------------------------------------------
/packages/cli/src/env.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | import envalid from 'envalid';
4 |
5 | import { constants } from '#libs/platform';
6 |
7 | export default envalid.cleanEnv(process.env, {
8 | DN_API_URL: envalid.url({
9 | default: 'https://datanaut.ai/',
10 | devDefault: 'http://localhost:3001/',
11 | desc: 'The URL of the datanaut API',
12 | }),
13 | DN_CLIENT_ID: envalid.str({
14 | default: 'openmcp-cli',
15 | }),
16 | DN_CONFIGDIR: envalid.str({
17 | default: path.join(constants.CONFIGDIR, 'datanaut'),
18 | }),
19 | });
20 |
--------------------------------------------------------------------------------
/packages/cli/src/errors/guards.ts:
--------------------------------------------------------------------------------
1 | export function isEnoentError(error: unknown): error is NodeJS.ErrnoException {
2 | return (
3 | typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT'
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/src/errors/handler-error.ts:
--------------------------------------------------------------------------------
1 | export default class HandlerError extends Error {
2 | override readonly message: string;
3 |
4 | constructor(error: unknown) {
5 | const message = error instanceof Error ? error.message : String(error);
6 | super(message, { cause: error });
7 | this.message = message;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export { isEnoentError } from './guards.ts';
2 | export { default as HandlerError } from './handler-error.ts';
3 | export { default as PrintableError } from './printable-error.ts';
4 | export { OperationCanceledError, OperationTimedOutError } from './ui-errors.ts';
5 |
--------------------------------------------------------------------------------
/packages/cli/src/errors/printable-error.ts:
--------------------------------------------------------------------------------
1 | export default class PrintableError extends Error {
2 | override readonly message: string;
3 |
4 | constructor(message: string, error: unknown) {
5 | super(message, { cause: error });
6 | this.message = `${message}: ${error instanceof Error ? error.message : String(error)}`;
7 | }
8 |
9 | override toString() {
10 | return this.message;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/errors/ui-errors.ts:
--------------------------------------------------------------------------------
1 | export class OperationCanceledError extends Error {
2 | constructor() {
3 | super('Operation was canceled');
4 | }
5 | }
6 |
7 | export class OperationTimedOutError extends Error {
8 | constructor(timeout: number) {
9 | super(`Operation timed out after ${timeout}ms`);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as install } from './commands/install/index.ts';
2 | export { default as run } from './commands/run/index.ts';
3 | export { default as uninstall } from './commands/uninstall/index.ts';
4 | export { default as upload } from './commands/upload/index.ts';
5 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/cli-utils/create-handler.ts:
--------------------------------------------------------------------------------
1 | import type { ArgumentsCamelCase } from 'yargs';
2 |
3 | import { HandlerError } from '#errors';
4 | import console from '#libs/console';
5 |
6 | import { render } from '../../ui/index.ts';
7 |
8 | type Handler = (args: ArgumentsCamelCase) => void | Promise;
9 |
10 | export default function createHandler = Handler>(fn: F, ui: boolean): F {
11 | const wrappedFn: Handler = async args => {
12 | console.wrapConsole();
13 | const inkInstance = ui ? render() : null;
14 | try {
15 | await fn(args);
16 | } catch (error) {
17 | if (inkInstance !== null) {
18 | console.error(error instanceof Error ? error.message : String(error));
19 | }
20 |
21 | throw new HandlerError(error);
22 | } finally {
23 | console.restoreConsole();
24 | inkInstance?.rerender();
25 | inkInstance?.unmount();
26 | }
27 | };
28 |
29 | return wrappedFn as F;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/cli-utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as createHandler } from './create-handler.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/confirm.ts:
--------------------------------------------------------------------------------
1 | import { setAndWaitForPrompt } from './state.ts';
2 | import type { ConfirmPrompt } from './types.ts';
3 |
4 | export default function confirm(config: ConfirmPrompt['config']): Promise {
5 | return setAndWaitForPrompt({
6 | type: 'confirm',
7 | config,
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/guards.ts:
--------------------------------------------------------------------------------
1 | import { isPlainObject } from '@stoplight/json';
2 |
3 | import type { MultiSelectPrompt, Prompt, SelectPrompt, TextPrompt } from './types.ts';
4 |
5 | export function isTextPrompt(prompt: unknown): prompt is TextPrompt {
6 | return isPlainObject(prompt) && prompt['type'] === 'text';
7 | }
8 |
9 | export function isConfirmPrompt(prompt: unknown): prompt is Prompt {
10 | return isPlainObject(prompt) && prompt['type'] === 'confirm';
11 | }
12 |
13 | export function isSelectPrompt(prompt: unknown): prompt is SelectPrompt {
14 | return isPlainObject(prompt) && prompt['type'] === 'select';
15 | }
16 |
17 | export function isMultiSelectPrompt(prompt: unknown): prompt is MultiSelectPrompt {
18 | return isPlainObject(prompt) && prompt['type'] === 'multi-select';
19 | }
20 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/index.ts:
--------------------------------------------------------------------------------
1 | export { default as confirm } from './confirm.ts';
2 | export { multiselect, select } from './select.ts';
3 | export * as state from './state.ts';
4 | export { maskedText, text } from './text.ts';
5 | export type {
6 | ConfirmPrompt,
7 | MultiSelectPrompt,
8 | Prompt,
9 | SelectPrompt,
10 | SelectPromptOption,
11 | TextPrompt,
12 | } from './types.ts';
13 | export { default as url } from './url.ts';
14 | export { password, username } from './user.ts';
15 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/select.ts:
--------------------------------------------------------------------------------
1 | import { setAndWaitForPrompt } from './state.ts';
2 | import type { MultiSelectPrompt, SelectPrompt } from './types.ts';
3 |
4 | export function select(config: SelectPrompt['config']): Promise {
5 | return setAndWaitForPrompt({
6 | type: 'select',
7 | config,
8 | });
9 | }
10 |
11 | export function multiselect(config: MultiSelectPrompt['config']): Promise {
12 | return setAndWaitForPrompt({
13 | type: 'multi-select',
14 | config,
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/text.ts:
--------------------------------------------------------------------------------
1 | import { setAndWaitForPrompt } from './state.ts';
2 | import type { TextPrompt } from './types.ts';
3 |
4 | export function text(config: TextPrompt['config']): Promise {
5 | return setAndWaitForPrompt({
6 | type: 'text',
7 | config,
8 | });
9 | }
10 |
11 | export function maskedText(config: Omit) {
12 | return text({
13 | ...config,
14 | mask: '*',
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/types.ts:
--------------------------------------------------------------------------------
1 | export type TextPrompt = {
2 | type: 'text';
3 | config: {
4 | message: string;
5 | placeholder?: string;
6 | mask?: string;
7 | help?: string;
8 | defaultValue?: string;
9 | validate?(value: string): string | void;
10 | };
11 | };
12 |
13 | export type ConfirmPrompt = {
14 | type: 'confirm';
15 | config: {
16 | message: string;
17 | defaultValue?: boolean;
18 | };
19 | };
20 |
21 | export type SelectPromptOption = {
22 | label: string;
23 | hint?: string;
24 | value: string;
25 | };
26 |
27 | export type SelectPrompt = {
28 | type: 'select';
29 | config: {
30 | message: string;
31 | initialValue?: string;
32 | options: SelectPromptOption[];
33 | };
34 | };
35 |
36 | export type MultiSelectPrompt = {
37 | type: 'multi-select';
38 | config: {
39 | message: string;
40 | initialValues?: string[];
41 | options: SelectPromptOption[];
42 | optional?: boolean;
43 | };
44 | };
45 |
46 | export type Prompt = TextPrompt | ConfirmPrompt | SelectPrompt | MultiSelectPrompt;
47 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/url.ts:
--------------------------------------------------------------------------------
1 | import { text } from './text.ts';
2 |
3 | export default function url(message = 'Please provide URL:'): Promise {
4 | return text({
5 | message,
6 | validate: url => {
7 | if (!URL.canParse(url)) {
8 | return 'Inserted invalid URL. Please provide a valid URL.';
9 | }
10 | },
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/prompts/user.ts:
--------------------------------------------------------------------------------
1 | import { maskedText, text } from './text.ts';
2 |
3 | export const username = (message = 'Please insert username:') =>
4 | text({
5 | message,
6 | validate: value => {
7 | if (value.trim().length === 0) {
8 | return 'Username cannot be empty';
9 | }
10 | },
11 | });
12 |
13 | export const password = (message = 'Please insert password:') =>
14 | maskedText({
15 | message,
16 | });
17 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/reporters/__tests__/log-file.spec.ts:
--------------------------------------------------------------------------------
1 | import { createConsola } from 'consola/core';
2 | import { describe, expect, it, vi } from 'vitest';
3 |
4 | import logFileReporter from '../log-file.ts';
5 |
6 | describe('logFileReporter', () => {
7 | it('should write log message to stdout for relevant log types', () => {
8 | const stdoutMock = { write: vi.fn() };
9 | const consola = createConsola({
10 | stdout: stdoutMock as any,
11 | reporters: [logFileReporter],
12 | });
13 |
14 | consola.info('info message', 0);
15 | consola.log('log message', 2);
16 |
17 | expect(stdoutMock.write).toHaveBeenCalledTimes(2);
18 | expect(stdoutMock.write.mock.calls).toEqual([
19 | [expect.stringMatching(/\[INFO]\sinfo message\s0\n/)],
20 | [expect.stringMatching(/\[LOG]\slog message\s2\n/)],
21 | ]);
22 | });
23 |
24 | it('should write log message to stderr for error log types', () => {
25 | const stderrMock = { write: vi.fn() };
26 | const consola = createConsola({
27 | stderr: stderrMock as any,
28 | reporters: [logFileReporter],
29 | });
30 |
31 | consola.error('error message', new Error('some error'));
32 |
33 | expect(stderrMock.write.mock.calls).toEqual([
34 | [expect.stringMatching(/\[ERROR]\serror message\sError: some error\n/)],
35 | ]);
36 | });
37 |
38 | it('should handle missing stdout and stderr gracefully', () => {
39 | const consola = createConsola({
40 | reporters: [logFileReporter],
41 | });
42 |
43 | expect(() => consola.error('error')).not.toThrow();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/reporters/console.ts:
--------------------------------------------------------------------------------
1 | import { type ConsolaReporter } from 'consola/core';
2 |
3 | const LOG_LEVELS = {
4 | 0: 'error',
5 | 1: 'warn',
6 | 2: 'log',
7 | 3: 'info',
8 | 4: 'debug',
9 | 5: 'trace',
10 | } as const;
11 |
12 | export default {
13 | log(logObject) {
14 | const args = logObject.message === undefined ? logObject.args : [logObject.message, ...logObject.args];
15 | // eslint-disable-next-line no-console
16 | console[LOG_LEVELS[logObject.level]](...args);
17 | },
18 | } as const satisfies ConsolaReporter;
19 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/reporters/fancy/fancy.ts:
--------------------------------------------------------------------------------
1 | import type { ConsolaReporter, LogObject, LogType as ConsolaLogType } from 'consola/core';
2 |
3 | import { createShallowObservableArray } from '#libs/observable';
4 |
5 | export type LogType = 'error' | 'warn' | 'info' | 'verbose' | 'start' | 'success';
6 |
7 | export type Log = {
8 | readonly id: string;
9 | readonly type: LogType;
10 | readonly message: string;
11 | readonly timestamp: Date;
12 | };
13 |
14 | export const logs = createShallowObservableArray([]);
15 |
16 | export function addLog(type: LogType, message: string, timestamp: Date) {
17 | logs.push({
18 | id: String(logs.length + 1),
19 | type,
20 | message,
21 | timestamp,
22 | });
23 | }
24 |
25 | const map = {
26 | error: 'error',
27 | fatal: 'error',
28 | fail: 'error',
29 |
30 | warn: 'warn',
31 |
32 | info: 'info',
33 | log: 'info',
34 |
35 | box: null,
36 | start: 'start',
37 |
38 | success: 'success',
39 | ready: 'success',
40 |
41 | debug: 'verbose',
42 | trace: 'verbose',
43 | verbose: 'verbose',
44 |
45 | silent: null,
46 | } as const satisfies Record;
47 |
48 | // Format the message from logObj
49 | function formatMessage(logObj: LogObject): string {
50 | const serializedArgs = [logObj.message, ...logObj.args].filter(v => v !== undefined).map(v => String(v));
51 |
52 | return serializedArgs.join(' ');
53 | }
54 |
55 | const fancyReporter: ConsolaReporter = {
56 | log(logObj) {
57 | const type = map[logObj.type];
58 | if (type === null) return;
59 | const message = formatMessage(logObj);
60 | addLog(type, message, new Date(logObj.date));
61 | },
62 | };
63 |
64 | export default fancyReporter;
65 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/reporters/fancy/index.ts:
--------------------------------------------------------------------------------
1 | export { default, logs, type LogType } from './fancy.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/reporters/log-file.ts:
--------------------------------------------------------------------------------
1 | import { type ConsolaReporter, LogLevels, type LogObject } from 'consola/core';
2 |
3 | function formatDate(date: Date): string {
4 | const year = date.getFullYear();
5 | const month = String(date.getMonth() + 1).padStart(2, '0');
6 | const day = String(date.getDate()).padStart(2, '0');
7 | const hours = String(date.getHours()).padStart(2, '0');
8 | const minutes = String(date.getMinutes()).padStart(2, '0');
9 | const seconds = String(date.getSeconds()).padStart(2, '0');
10 |
11 | const timezoneOffset = -date.getTimezoneOffset();
12 | const timezoneHours = String(Math.floor(Math.abs(timezoneOffset) / 60)).padStart(2, '0');
13 | const timezoneMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, '0');
14 | const timezoneSign = timezoneOffset >= 0 ? '+' : '-';
15 |
16 | const timezone = `${timezoneSign}${timezoneHours}:${timezoneMinutes}`;
17 |
18 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
19 | }
20 |
21 | function formatMessage(logObj: LogObject) {
22 | const date = formatDate(logObj.date);
23 | const serializedArgs = [logObj.message, ...logObj.args].filter(v => v !== undefined).map(v => String(v));
24 | return `[${date}] [${logObj.type.toUpperCase()}] ${serializedArgs.join(' ')}\n`;
25 | }
26 |
27 | export default {
28 | log(logObj, { options }) {
29 | if (logObj.level <= LogLevels.error) {
30 | options.stderr?.write(formatMessage(logObj));
31 | } else {
32 | options.stdout?.write(formatMessage(logObj));
33 | }
34 | },
35 | } as const satisfies ConsolaReporter;
36 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/console/types.ts:
--------------------------------------------------------------------------------
1 | import type { ConsolaInstance } from 'consola/core';
2 |
3 | export type PromptlessConsola = Omit;
4 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/agent.ts:
--------------------------------------------------------------------------------
1 | import { isDefinedError, ORPCError } from '@orpc/client';
2 |
3 | import { confirm } from '#libs/console/prompts';
4 | import { login } from '#libs/datanaut-auth-cli';
5 |
6 | import type { Agent } from '../../rpc/agents.ts';
7 | import { rpcClient } from './sdk/sdk.ts';
8 |
9 | type Opts = {
10 | /**
11 | * @defaultValue true
12 | */
13 | login?: boolean;
14 | };
15 |
16 | export async function getAgentById(id: string, { login: shouldLogin = true }: Opts = {}): Promise {
17 | let agent: Agent;
18 | try {
19 | agent = await rpcClient.cli.agents.getAgent({ agentId: id });
20 | } catch (error) {
21 | if (!(error instanceof ORPCError) || !isDefinedError(error)) {
22 | throw error;
23 | }
24 |
25 | if (error.status !== 401 || !shouldLogin) {
26 | throw new Error(error.message, { cause: error });
27 | }
28 |
29 | const res = await confirm({
30 | message: `You do not seem to be logged in. Would you like to log in?`,
31 | });
32 |
33 | if (res) {
34 | await login();
35 | // let's retry once logged in
36 | return getAgentById(id, { login: false });
37 | } else {
38 | throw new Error('Login cancelled. Please execute `login` to log in.');
39 | }
40 | }
41 |
42 | return agent;
43 | }
44 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/auth-cli.ts:
--------------------------------------------------------------------------------
1 | // a set of wrappers around auth
2 | import console from '#libs/console';
3 | import { confirm } from '#libs/console/prompts';
4 |
5 | import { login as _login, logout as _logout, whoami as _whoami } from './auth/index.ts';
6 |
7 | export async function login(): Promise {
8 | console.start('Logging in...');
9 | const { email } = await _login({
10 | async openPage(url: URL): Promise {
11 | const res = await confirm({
12 | message: `Do you want to open the login page in your browser?`,
13 | });
14 |
15 | if (!res) {
16 | console.info(['Copy the URL from the terminal and open it in the browser', url.toString()].join('\n'));
17 | } else {
18 | console.info(
19 | [
20 | 'Opening the login page in your browser...',
21 | 'If the browser does not open, copy the URL from the terminal and open it in the browser',
22 | url.toString(),
23 | ].join('\n'),
24 | );
25 | }
26 |
27 | return res;
28 | },
29 | });
30 | console.success(`Logged in successfully as ${email}`);
31 | }
32 |
33 | export async function logout(): Promise {
34 | console.start('Logging out...');
35 | await _logout();
36 | console.success('Logged out successfully');
37 | }
38 |
39 | export async function whoami(): Promise {
40 | const { email } = _whoami();
41 | console.success(`You are logged in as ${email}`);
42 | }
43 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/auth/api.ts:
--------------------------------------------------------------------------------
1 | import { clearTimeout, setTimeout } from 'node:timers';
2 |
3 | import open from 'open';
4 |
5 | import env from '../../../env.ts';
6 | import { createAuthClient } from './client.ts';
7 | import { startServer, waitForAuthorizationCallback } from './server.ts';
8 | import Storage from './storage/index.ts';
9 |
10 | export const client = await createAuthClient({
11 | hostURL: env.DN_API_URL,
12 | clientId: env.DN_CLIENT_ID,
13 | storage: await Storage.create('auth'),
14 | });
15 |
16 | interface Prompts {
17 | openPage(url: URL): Promise;
18 | }
19 |
20 | export function whoami() {
21 | const decoded = client.getDecodedIdToken();
22 | return {
23 | email: String(decoded['email']),
24 | } as const;
25 | }
26 |
27 | export async function login(prompts: Prompts) {
28 | const LOGIN_TIMEOUT = 1000 * 60 * 5; // 5 minutes
29 |
30 | const controller = new AbortController();
31 | const tId = setTimeout(() => controller.abort(), LOGIN_TIMEOUT);
32 | using server = await startServer(controller.signal);
33 | try {
34 | const authorizeUrl = await client.initiateAuthFlow(server.address);
35 |
36 | await Promise.all([
37 | waitForAuthorizationCallback(server, client).then(code => client.exchangeCodeForTokens(code)),
38 | prompts.openPage(authorizeUrl).then(consent => {
39 | if (consent) {
40 | return open(authorizeUrl.toString());
41 | }
42 | }),
43 | ]);
44 |
45 | return whoami();
46 | } finally {
47 | clearTimeout(tId);
48 | }
49 | }
50 |
51 | export async function logout() {
52 | await client.clearTokens();
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/auth/index.ts:
--------------------------------------------------------------------------------
1 | export { client, login, logout, whoami } from './api.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/auth/storage/index.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from '../../fs-config.ts';
2 | import type { OAuth2Tokens, Storage } from '../client.ts';
3 | import Mutex from './mutex.ts';
4 |
5 | export default class StorageImpl implements Storage {
6 | readonly #filename: string;
7 | readonly #data: OAuth2Tokens;
8 | readonly #mutex = new Mutex();
9 |
10 | constructor(filename: string, data: Record) {
11 | this.#filename = filename;
12 | this.#data = data;
13 | }
14 |
15 | /**
16 | * Creates a new storage instance
17 | *
18 | * @param namespace
19 | */
20 | static async create(namespace: string) {
21 | const filename = `storage-${namespace}.json`;
22 | const data = await readFile(filename);
23 | try {
24 | return new StorageImpl(filename, data === null ? {} : JSON.parse(data));
25 | } catch {
26 | return new StorageImpl(filename, {});
27 | }
28 | }
29 |
30 | async #flush() {
31 | await writeFile(this.#filename, JSON.stringify(this.#data));
32 | }
33 |
34 | async setItem(key: string, value: unknown): Promise {
35 | await this.#mutex.lock(async () => {
36 | this.#data[key as string] = value as string;
37 | await this.#flush();
38 | });
39 | }
40 |
41 | async getItem(key: K): Promise {
42 | return this.#data[key] ?? null;
43 | }
44 |
45 | async removeItem(key: string): Promise {
46 | await this.#mutex.lock(async () => {
47 | delete this.#data[key as string];
48 | await this.#flush();
49 | });
50 | }
51 |
52 | async clear() {
53 | await this.#mutex.lock(async () => {
54 | for (const key of Object.keys(this.#data)) {
55 | delete this.#data[key];
56 | }
57 | await this.#flush();
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/auth/storage/mutex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A simple mutex implementation for asynchronous functions
3 | * Note: this is not a general purpose mutex implementation, it is only
4 | * intended to be used for synchronizing access to a single resource.
5 | */
6 | export default class Mutex {
7 | private mutex = Promise.resolve();
8 |
9 | /**
10 | * Locks the mutex and executes the given function
11 | * @param fn The function to execute while the mutex is locked
12 | * @returns The result of the function
13 | */
14 | async lock(fn: () => Promise): Promise {
15 | const { promise: newMutex, resolve: unlock } = Promise.withResolvers();
16 |
17 | // Wait for the previous operation to complete
18 | const previousMutex = this.mutex;
19 | this.mutex = previousMutex.then(() => newMutex);
20 |
21 | try {
22 | await previousMutex;
23 | return await fn();
24 | } finally {
25 | unlock();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/fs-config.ts:
--------------------------------------------------------------------------------
1 | import { mkdir, readFile as fsReadFile, writeFile as fsWriteFile } from 'node:fs/promises';
2 | import { join } from 'node:path';
3 |
4 | import env from '../../env.ts';
5 |
6 | /**
7 | * Gets the path to a file in the configuration directory
8 | * @param filename The name of the file
9 | * @returns The full path to the file
10 | */
11 | export function getFilePath(filename: string): string {
12 | return join(env.DN_CONFIGDIR, filename);
13 | }
14 |
15 | /**
16 | * Reads data from a file in the configuration directory
17 | * @param filename The name of the file to read
18 | * @returns The file contents as a string, or null if the file doesn't exist
19 | */
20 | export async function readFile(filename: string): Promise {
21 | try {
22 | return await fsReadFile(getFilePath(filename), 'utf-8');
23 | } catch {
24 | return null;
25 | }
26 | }
27 |
28 | /**
29 | * Writes data to a file in the configuration directory
30 | * @param filename The name of the file to write
31 | * @param data The data to write to the file
32 | */
33 | export async function writeFile(filename: string, data: string): Promise {
34 | await ensureConfigDirExists();
35 | await fsWriteFile(getFilePath(filename), data, 'utf8');
36 | }
37 |
38 | /**
39 | * Ensures that the configuration directory exists
40 | */
41 | async function ensureConfigDirExists(): Promise {
42 | await mkdir(env.DN_CONFIGDIR, { recursive: true });
43 | }
44 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/datanaut/sdk/sdk.ts:
--------------------------------------------------------------------------------
1 | import { createORPCClient } from '@orpc/client';
2 | import { RPCLink } from '@orpc/client/fetch';
3 | import { SimpleCsrfProtectionLinkPlugin } from '@orpc/client/plugins';
4 |
5 | import env from '../../../env.ts';
6 | import type { ContractRouterClient } from '../../../rpc/index.ts';
7 | import { client } from '../auth/index.ts';
8 |
9 | const link = new RPCLink({
10 | url: new URL('__rpc', env.DN_API_URL),
11 | plugins: [new SimpleCsrfProtectionLinkPlugin()],
12 | async headers() {
13 | try {
14 | const bearer = await client.generateAccessToken();
15 | return {
16 | Authorization: `Bearer ${bearer}`,
17 | 'X-Bearer-Format': 'opaque',
18 | };
19 | } catch {
20 | return {};
21 | }
22 | },
23 | });
24 |
25 | export const rpcClient: ContractRouterClient = createORPCClient(link);
26 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/config/index.ts:
--------------------------------------------------------------------------------
1 | export { default as readConfig } from './read.ts';
2 | export { type ResolvableConfigPath, default as resolveConfigPath } from './resolve-path.ts';
3 | export { default as writeConfig } from './write.ts';
4 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/config/read.ts:
--------------------------------------------------------------------------------
1 | import { parseDocument } from '@openmcp/utils/documents';
2 | import type { z } from 'zod';
3 |
4 | import type { Context, FsInstallMethod } from '../types.ts';
5 | import resolveConfigPath from './resolve-path.ts';
6 |
7 | export default async function readConfig(
8 | { constants, fs, logger }: Context,
9 | installMethod: I,
10 | ): Promise> {
11 | const resolvedConfigPath = resolveConfigPath(constants, installMethod.filepath);
12 | logger.start(`Loading config from "${resolvedConfigPath}"`);
13 | const content = await fs.readFile(resolvedConfigPath, 'utf8');
14 | const fileExtension = resolvedConfigPath.split('.').pop() || '';
15 | let document;
16 | try {
17 | document = parseDocument(content, fileExtension);
18 | } catch (error) {
19 | throw new Error(`Error parsing config file: ${error instanceof Error ? error.message : String(error)}`);
20 | }
21 |
22 | const result = installMethod.schema.safeParse(document);
23 | if (result.error) {
24 | throw new Error(`Error validating config file: ${result.error.issues.map(issue => issue.message).join(', ')}`);
25 | }
26 |
27 | logger.success('Config was loaded successfully');
28 |
29 | return result.data;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/config/resolve-path.ts:
--------------------------------------------------------------------------------
1 | import { normalize } from 'node:path';
2 | import { join } from 'node:path/posix';
3 |
4 | import type { Context } from '../types.ts';
5 |
6 | export type ResolvableConfigPath = `${'$CWD' | '$HOME' | '$CONFIG' | '$VSCODE' | ''}/${string}`;
7 |
8 | export default function resolveConfigPath(
9 | { HOMEDIR, CONFIGDIR, CWD }: Context['constants'],
10 | value: ResolvableConfigPath,
11 | ) {
12 | return normalize(
13 | value.replace(/^\$[A-Za-z_]+/g, v => {
14 | switch (v) {
15 | case '$HOME':
16 | return HOMEDIR;
17 | case '$CONFIG':
18 | return CONFIGDIR;
19 | case '$CWD':
20 | return CWD;
21 | case '$VSCODE':
22 | return join(CONFIGDIR, 'Code', 'User');
23 | default:
24 | throw new Error(`Unknown path variable: ${v}`);
25 | }
26 | }),
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/config/write.ts:
--------------------------------------------------------------------------------
1 | import { dirname } from 'node:path';
2 |
3 | import { serializeDocument } from '@openmcp/utils/documents';
4 | import type { z } from 'zod';
5 |
6 | import { isEnoentError } from '../../../errors/guards.ts';
7 | import type { Context, FsInstallMethod } from '../types.ts';
8 | import readConfig from './read.ts';
9 | import resolveConfigPath from './resolve-path.ts';
10 |
11 | export default async function writeConfig(
12 | ctx: Context,
13 | installMethod: I,
14 | applyConfig: (config: z.infer, filepath: string) => Promise,
15 | ): Promise {
16 | const resolvedConfigPath = resolveConfigPath(ctx.constants, installMethod.filepath);
17 | let config: z.infer;
18 | try {
19 | config = await readConfig(ctx, installMethod);
20 | } catch (error) {
21 | if (!isEnoentError(error)) {
22 | throw error;
23 | }
24 |
25 | ctx.logger.info('Config does not exist yet');
26 | await ctx.fs.mkdir(dirname(resolvedConfigPath), { recursive: true });
27 | config = {};
28 | }
29 |
30 | await applyConfig(config, resolvedConfigPath);
31 | const ext = resolvedConfigPath.slice(resolvedConfigPath.lastIndexOf('.') + 1);
32 | if (ext === 'json') {
33 | await ctx.fs.writeFile(resolvedConfigPath, serializeDocument(config, 'json'));
34 | } else {
35 | await ctx.fs.writeFile(resolvedConfigPath, serializeDocument(config, 'yaml'));
36 | }
37 |
38 | ctx.logger.success('Config was saved successfully');
39 | }
40 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/errors/guards.ts:
--------------------------------------------------------------------------------
1 | export function isEnoentError(error: unknown): error is NodeJS.ErrnoException {
2 | return (
3 | typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT'
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/errors/index.ts:
--------------------------------------------------------------------------------
1 | export { isEnoentError } from './guards.ts';
2 | export { default as InstallLocationUnavailable } from './install-method-unavailable.ts';
3 | export { default as ServerConflict } from './server-conflict.ts';
4 | export { default as ServerNotInstalled } from './server-not-installed.ts';
5 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/errors/install-method-unavailable.ts:
--------------------------------------------------------------------------------
1 | import type { InstallMethodLocation, Server } from '../types.ts';
2 |
3 | export default class InstallLocationUnavailable extends Error {
4 | constructor(server: Server, location: InstallMethodLocation) {
5 | super(`Install location "${location}" is not available for server "${server.name}"`);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/errors/server-conflict.ts:
--------------------------------------------------------------------------------
1 | import type { Server } from '../types.ts';
2 |
3 | export default class ServerConflict extends Error {
4 | constructor(server: Server) {
5 | super(`Server "${server.name}" is already installed.`);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/errors/server-not-installed.ts:
--------------------------------------------------------------------------------
1 | import type { Remix } from '../types.ts';
2 |
3 | export default class RemixNotInstalled extends Error {
4 | constructor(remix: Remix) {
5 | super(`Server "${remix.name}" is not installed`);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/get-install-filepath.ts:
--------------------------------------------------------------------------------
1 | import { constants as osConstants } from '#libs/platform';
2 |
3 | import { resolveConfigPath } from './config/index.ts';
4 | import { type IntegrationName, integrations } from './integrations/index.ts';
5 | import findMatchingInstallMethod from './integrations/utils/find-matching-install-method.ts';
6 | import type { InstallLocation } from './types.ts';
7 |
8 | export default function getInstallFilepath(
9 | cwd: string,
10 | integrationName: IntegrationName,
11 | location: InstallLocation,
12 | ): string | null {
13 | const integration = integrations[integrationName];
14 | const installMethod = findMatchingInstallMethod(integration.installMethods, location);
15 | if (installMethod === null) return null;
16 |
17 | const constants = {
18 | ...osConstants,
19 | CWD: cwd,
20 | } as const;
21 | return resolveConfigPath(constants, installMethod.filepath);
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/get-install-hints.ts:
--------------------------------------------------------------------------------
1 | import type { IntegrationName } from './integrations/index.ts';
2 | import generateServerName from './integrations/utils/generate-server-name.ts';
3 | import type { Server } from './types.ts';
4 |
5 | type ShellCommandHint = {
6 | readonly type: 'command';
7 | readonly value: string;
8 | };
9 |
10 | type LinkCommandHint = {
11 | readonly type: 'url';
12 | readonly value: string;
13 | };
14 |
15 | type Hint = ShellCommandHint | LinkCommandHint;
16 |
17 | function generateVsCodeTransport(server: Server) {
18 | return serializeTransport({
19 | name: generateServerName([], server),
20 | type: 'stdio',
21 | args: ['-y', 'openmcp@latest', 'run', '--server', server.target],
22 | });
23 | }
24 |
25 | function serializeTransport(transport: Record) {
26 | return encodeURIComponent(JSON.stringify(transport));
27 | }
28 |
29 | export default function getInstallHints(
30 | server: Server,
31 | integrationName: IntegrationName,
32 | ): [ShellCommandHint] | [ShellCommandHint, LinkCommandHint] {
33 | const hints: [ShellCommandHint, ...Hint[]] = [
34 | {
35 | type: 'command',
36 | value: `npx openmcp@latest install ${server.target} --client ${integrationName}`,
37 | },
38 | ];
39 |
40 | if (integrationName === 'vscode') {
41 | hints.push({
42 | type: 'url',
43 | value: `vscode:mcp/install?${generateVsCodeTransport(server)}`,
44 | });
45 | } else if (integrationName === 'vscode-insiders') {
46 | hints.push({
47 | type: 'url',
48 | value: `vscode-insiders:mcp/install?${generateVsCodeTransport(server)}`,
49 | });
50 | }
51 |
52 | return hints as [ShellCommandHint] | [ShellCommandHint, LinkCommandHint];
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/index.isomorphic.ts:
--------------------------------------------------------------------------------
1 | export { default as getInstallHints } from './get-install-hints.ts';
2 | export type { IntegrationName } from './integrations/index.ts';
3 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/index.ts:
--------------------------------------------------------------------------------
1 | export { default as getInstallFilepath } from './get-install-filepath.ts';
2 | export { default as getInstallHints } from './get-install-hints.ts';
3 | export { default as install } from './install.ts';
4 | export { type IntegrationName, integrations } from './integrations/index.ts';
5 | export type { InstallLocation, Logger, McpHostClient, Remix } from './types.ts';
6 | export { default as uninstall } from './uninstall.ts';
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/install.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs/promises';
2 |
3 | import { constants as osConstants, getPlatform as platform } from '#libs/platform';
4 |
5 | import resolveConfigPath from './config/resolve-path.ts';
6 | import { ServerConflict } from './errors/index.ts';
7 | import { type IntegrationName, integrations } from './integrations/index.ts';
8 | import type { InstallLocation, Logger, Server } from './types.ts';
9 |
10 | export default async function install(
11 | {
12 | cwd,
13 | logger,
14 | }: {
15 | readonly cwd: string;
16 | readonly logger: Logger;
17 | },
18 | integrationName: IntegrationName,
19 | server: Server,
20 | location: InstallLocation,
21 | ): Promise {
22 | const integration = integrations[integrationName];
23 | const serverName = JSON.stringify(server.name);
24 | logger.start(`Installing ${serverName}`);
25 | try {
26 | const constants = {
27 | ...osConstants,
28 | CWD: cwd,
29 | } as const;
30 | const { filepath } = await integration.install(
31 | {
32 | platform: platform(),
33 | constants,
34 | fs,
35 | logger,
36 | },
37 | server,
38 | location,
39 | );
40 |
41 | const resolvedConfigPath = resolveConfigPath(constants, filepath);
42 | logger.success(`${serverName} was successfully installed to ${JSON.stringify(resolvedConfigPath)}`);
43 | } catch (error) {
44 | if (error instanceof ServerConflict) {
45 | logger.info(
46 | `${serverName} is already installed. You may need to restart your target client for changes to take affect.`,
47 | );
48 | return;
49 | }
50 |
51 | logger.error(new Error(`Failed to install ${serverName}`, { cause: error }));
52 | return;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/__tests__/fixtures/generic.ts:
--------------------------------------------------------------------------------
1 | import { serializeDocument } from '@openmcp/utils/documents';
2 |
3 | export const configYaml = serializeDocument(
4 | {
5 | mcpServers: {
6 | 'test-server': {
7 | command: 'npx',
8 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc123de'],
9 | },
10 | },
11 | },
12 | 'yaml',
13 | );
14 |
15 | export const emptyConfigYaml = serializeDocument(
16 | {
17 | mcpServers: {},
18 | },
19 | 'yaml',
20 | );
21 |
22 | export const invalidConfigYaml = serializeDocument(
23 | {
24 | mcpServers: {
25 | 'test-server': {
26 | command: 'invalid-command', // Not 'npx', so it won't match SERVER_SCHEMA
27 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc123de'],
28 | },
29 | 'valid-server': {
30 | command: 'npx',
31 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_xyz789'],
32 | },
33 | },
34 | },
35 | 'yaml',
36 | );
37 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/__tests__/fixtures/goose.ts:
--------------------------------------------------------------------------------
1 | import { serializeDocument } from '@openmcp/utils/documents';
2 |
3 | export const configYaml = serializeDocument(
4 | {
5 | extensions: {
6 | 'test-server': {
7 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc123de'],
8 | bundled: null,
9 | cmd: 'npx',
10 | description: null,
11 | enabled: true,
12 | env_keys: [],
13 | envs: {},
14 | name: 'Test Server',
15 | timeout: 300,
16 | type: 'stdio',
17 | },
18 | },
19 | },
20 | 'yaml',
21 | );
22 |
23 | export const emptyConfigYaml = serializeDocument(
24 | {
25 | extensions: {},
26 | },
27 | 'yaml',
28 | );
29 |
30 | export const invalidConfigYaml = serializeDocument(
31 | {
32 | extensions: {
33 | 'test-server': {
34 | args: ['-y', 'openmcp@latest', 'run', '--server', 'ag_abc123de'],
35 | bundled: null,
36 | cmd: 'invalid-command', // Not 'npx', so it won't match SERVER_SCHEMA
37 | description: null,
38 | enabled: true,
39 | env_keys: [],
40 | envs: {},
41 | name: 'Test Server',
42 | timeout: 300,
43 | type: 'stdio',
44 | },
45 | 'valid-server': {
46 | args: ['-y', 'openmcp@latest', 'run', '--server', 'ag_xyz789'],
47 | bundled: null,
48 | cmd: 'npx',
49 | description: null,
50 | enabled: true,
51 | env_keys: [],
52 | envs: {},
53 | name: 'Valid Server',
54 | timeout: 300,
55 | type: 'stdio',
56 | },
57 | },
58 | },
59 | 'yaml',
60 | );
61 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/__tests__/fixtures/vscode.ts:
--------------------------------------------------------------------------------
1 | export const settingsJson = JSON.stringify(
2 | {
3 | mcp: {
4 | servers: {
5 | 'test-server': {
6 | type: 'stdio',
7 | command: 'npx',
8 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc123de'],
9 | },
10 | },
11 | },
12 | },
13 | null,
14 | 2,
15 | );
16 |
17 | export const emptySettingsJson = JSON.stringify(
18 | {
19 | mcp: {
20 | servers: {},
21 | },
22 | },
23 | null,
24 | 2,
25 | );
26 |
27 | export const mcpJson = JSON.stringify(
28 | {
29 | servers: {
30 | 'test-server': {
31 | type: 'stdio',
32 | command: 'npx',
33 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc123de'],
34 | },
35 | },
36 | },
37 | null,
38 | 2,
39 | );
40 |
41 | export const emptyMcpJson = JSON.stringify(
42 | {
43 | servers: {},
44 | },
45 | null,
46 | 2,
47 | );
48 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/cursor.ts:
--------------------------------------------------------------------------------
1 | import { inferTargetType } from '#libs/mcp-utils';
2 |
3 | import { resolveConfigPath } from '../config/index.ts';
4 | import type { Context, FsInstallMethod, InstallMethod, McpHostClient, Server } from '../types.ts';
5 | import createGenericClient from './generic.ts';
6 | import generateDefinitionWorkspacePath from './utils/generate-definition-workspace-path.ts';
7 | import unwrapMatchingInstallMethod from './utils/uwrap-matching-install-method.ts';
8 |
9 | export default function createCursorClient(): McpHostClient {
10 | const client = createGenericClient('cursor', {
11 | global: '$HOME/.cursor/mcp.json',
12 | local: '$CWD/.cursor/mcp.json',
13 | });
14 |
15 | return {
16 | name: client.name,
17 | installMethods: client.installMethods,
18 | install(ctx, server, location) {
19 | if (location === 'local' || location === 'prefer-local') {
20 | return client.install(ctx, resolveServer(ctx, client.installMethods, server), location);
21 | }
22 |
23 | return client.install(ctx, server, location);
24 | },
25 | uninstall(ctx, server, location) {
26 | if (location === 'local' || location === 'prefer-local') {
27 | return client.uninstall(ctx, resolveServer(ctx, client.installMethods, server), location);
28 | }
29 |
30 | return client.uninstall(ctx, server, location);
31 | },
32 | };
33 | }
34 |
35 | function resolveServer(ctx: Context, installMethods: InstallMethod[], server: S): S {
36 | const installMethod = unwrapMatchingInstallMethod(installMethods, server, 'local');
37 | const resolvedPath = resolveConfigPath(ctx.constants, installMethod.filepath);
38 | if (inferTargetType(server.target) !== 'openapi') {
39 | return server;
40 | }
41 |
42 | return {
43 | ...server,
44 | target: generateDefinitionWorkspacePath('${workspaceFolder}/.cursor', resolvedPath, server.target),
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/index.ts:
--------------------------------------------------------------------------------
1 | import createCursorClient from './cursor.ts';
2 | import createGenericClient from './generic.ts';
3 | import createGooseClient from './goose.ts';
4 | import createVSCodeClient from './vscode.ts';
5 |
6 | export const integrations = {
7 | boltai: createGenericClient('boltai', {
8 | // https://docs.boltai.com/docs/plugins/mcp-servers#faqs
9 | global: '$HOME/.boltai/mcp.json',
10 | local: null,
11 | }),
12 | claude: createGenericClient('claude', {
13 | // https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server
14 | global: '$CONFIG/Claude/claude_desktop_config.json',
15 | local: null,
16 | }),
17 | cline: createGenericClient('cline', {
18 | global: '$VSCODE/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
19 | local: null,
20 | }),
21 | cursor: createCursorClient(),
22 | goose: createGooseClient(),
23 | roocode: createGenericClient('roocode', {
24 | global: '$VSCODE/rooveterinaryinc.roo-cline/settings/mcp_settings.json',
25 | local: null,
26 | }),
27 | vscode: createVSCodeClient('vscode', {
28 | global: '$VSCODE/settings.json',
29 | // https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server
30 | local: '$CWD/.vscode/mcp.json',
31 | }),
32 | 'vscode-insiders': createVSCodeClient('vscode-insiders', {
33 | global: '$CONFIG/Code - Insiders/User/settings.json',
34 | // https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server
35 | local: '$CWD/.vscode/mcp.json',
36 | }),
37 | windsurf: createGenericClient('windsurf', {
38 | // https://docs.windsurf.com/windsurf/mcp#mcp-config-json
39 | global: '$HOME/.codeium/windsurf/mcp_config.json',
40 | local: null,
41 | }),
42 | witsy: createGenericClient('witsy', {
43 | global: '$CONFIG/Witsy/settings.json',
44 | local: null,
45 | }),
46 | } as const;
47 |
48 | export { default as generateRemixName } from './utils/generate-server-name.ts';
49 |
50 | export type IntegrationName = keyof typeof integrations;
51 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/__tests__/generate-transport.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 |
3 | import type { InstallMethodLocation, Server } from '../../../types.ts';
4 | import generateTransport from '../generate-transport.ts';
5 |
6 | describe('generateTransport', () => {
7 | it('should generate transport with --server flag when target starts with ag_', () => {
8 | const server: Server = {
9 | id: 'ag_abc1234de',
10 | name: 'Test Server',
11 | target: 'ag_abc1234de',
12 | };
13 |
14 | const transport = generateTransport(server);
15 |
16 | expect(transport).toStrictEqual({
17 | command: 'npx',
18 | args: ['-y', 'openmcp@1', 'run', '--server', 'ag_abc1234de'],
19 | });
20 | });
21 |
22 | it('should generate transport with --config flag when target does not start with ag_', () => {
23 | const server: Server = {
24 | id: 'test-server-id',
25 | name: 'Test Server',
26 | target: '/path/to/config.json',
27 | };
28 |
29 | const transport = generateTransport(server);
30 |
31 | expect(transport).toStrictEqual({
32 | command: 'npx',
33 | args: ['-y', 'openmcp@1', 'run', '--config', '/path/to/config.json'],
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/find-existing-server.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | import type { Server } from '../../types.ts';
4 | import type { InstalledServer } from './types.ts';
5 |
6 | function _findExistingServer(configFilepath: string, installedServer: InstalledServer, server: Server): boolean {
7 | if (installedServer.args.length <= 3) {
8 | return false;
9 | }
10 |
11 | let isOpenmcpCli = false;
12 | let currentFlag: string = '';
13 | for (let i = 0; i < installedServer.args.length; i++) {
14 | const arg = installedServer.args[i]!;
15 | if (!isOpenmcpCli) {
16 | isOpenmcpCli ||= arg.startsWith('openmcp');
17 | continue;
18 | }
19 |
20 | if (arg.startsWith('-')) {
21 | currentFlag = arg;
22 | continue;
23 | }
24 |
25 | switch (currentFlag) {
26 | case '--server':
27 | return arg === server.target;
28 | case '--config':
29 | return (
30 | (arg.startsWith('$') && arg === server.target) ||
31 | (path.isAbsolute(arg) ? arg : path.join(configFilepath, '..', arg)) === server.target
32 | );
33 | }
34 | }
35 |
36 | return false;
37 | }
38 |
39 | export default function findExistingServer(
40 | configFilepath: string,
41 | installedServers: readonly InstalledServer[],
42 | server: Server,
43 | ): number {
44 | return installedServers.findIndex(installedServer => _findExistingServer(configFilepath, installedServer, server));
45 | }
46 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/find-matching-install-method.ts:
--------------------------------------------------------------------------------
1 | import type { InstallLocation, InstallMethod } from '../../types.ts';
2 |
3 | export default function findMatchingInstallMethod(
4 | methods: I[],
5 | location: InstallLocation,
6 | ): I | null {
7 | let globalMethod: I | null = null;
8 | for (const method of methods) {
9 | if (method.location === location) {
10 | return method;
11 | }
12 |
13 | if (method.location === 'local' && location === 'prefer-local') {
14 | return method;
15 | }
16 |
17 | if (method.location === 'global') {
18 | globalMethod = method;
19 | }
20 | }
21 |
22 | if (globalMethod !== null && location === 'prefer-local') {
23 | return globalMethod;
24 | }
25 |
26 | return null;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/generate-definition-workspace-path.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | export default function generateDefinitionWorkspacePath(variable: string, configFilepath: string, target: string) {
4 | if (URL.canParse(target)) {
5 | return target;
6 | }
7 |
8 | return `${variable}/${path.relative(path.dirname(configFilepath), target)}`;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/generate-server-name.ts:
--------------------------------------------------------------------------------
1 | import slugify from '@sindresorhus/slugify';
2 |
3 | import type { Server } from '../../types.ts';
4 | import type { InstalledServer } from './types.ts';
5 |
6 | export default function generateServerName(installedServers: readonly InstalledServer[], server: Server): string {
7 | const names = installedServers.map(s => s.name);
8 | const base = slugify(server.name);
9 | let i = 1;
10 | let name = base;
11 | while (names.includes(name)) {
12 | name = `${base}-${++i}`;
13 | }
14 |
15 | return name;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/generate-transport.ts:
--------------------------------------------------------------------------------
1 | import { inferTargetType } from '#libs/mcp-utils';
2 |
3 | import type { Server } from '../../types.ts';
4 |
5 | export default function generateTransport(server: Server) {
6 | const args = ['-y', 'openmcp@1', 'run'];
7 | // @todo: remove --config / --server flags once run is updated
8 | switch (inferTargetType(server.target)) {
9 | case 'agent-id':
10 | args.push('--server', server.target);
11 | break;
12 | case 'openapi':
13 | args.push('--config', server.target);
14 | break;
15 | }
16 |
17 | return {
18 | command: 'npx',
19 | args,
20 | } as const;
21 | }
22 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/guards.ts:
--------------------------------------------------------------------------------
1 | import { ServerConflict } from '../../errors/index.ts';
2 | import type { Server } from '../../types.ts';
3 | import findExistingServer from './find-existing-server.ts';
4 | import type { InstalledServer } from './types.ts';
5 |
6 | export function assertNoExistingServer(
7 | configFilepath: string,
8 | installedServers: readonly InstalledServer[],
9 | server: Server,
10 | ) {
11 | if (findExistingServer(configFilepath, installedServers, server) >= 0) {
12 | throw new ServerConflict(server);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/parse-generic-server.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const SERVER_SCHEMA = z
4 | .object({
5 | command: z.literal('npx'),
6 | args: z.array(z.string()),
7 | })
8 | .passthrough();
9 |
10 | export default function parseGenericServer(maybeServer: unknown): z.infer | null {
11 | const result = SERVER_SCHEMA.safeParse(maybeServer);
12 | if (!result.success) {
13 | return null;
14 | }
15 |
16 | return result.data;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type InstalledServer = {
2 | readonly name: string;
3 | readonly command: 'npx' | 'node';
4 | readonly args: readonly string[];
5 | };
6 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/integrations/utils/uwrap-matching-install-method.ts:
--------------------------------------------------------------------------------
1 | import InstallLocationUnavailable from '../../errors/install-method-unavailable.ts';
2 | import type { InstallLocation, InstallMethod, Server } from '../../types.ts';
3 | import findMatchingInstallMethod from './find-matching-install-method.ts';
4 |
5 | export default function unwrapMatchingInstallMethod(
6 | methods: I[],
7 | server: Server,
8 | location: InstallLocation,
9 | ): I {
10 | const method = findMatchingInstallMethod(methods, location);
11 | if (method === null) {
12 | throw new InstallLocationUnavailable(server, location === 'prefer-local' ? 'local' : location);
13 | }
14 |
15 | return method;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/types.ts:
--------------------------------------------------------------------------------
1 | import type * as fs from 'node:fs/promises';
2 |
3 | import type { z } from 'zod';
4 |
5 | import type { ResolvableConfigPath } from './config/index.ts';
6 |
7 | export type Constants = {
8 | readonly HOMEDIR: string;
9 | readonly CONFIGDIR: string;
10 | };
11 |
12 | export type Context = {
13 | readonly platform: 'darwin' | 'linux' | 'unix' | 'win32';
14 | readonly constants: Constants & { readonly CWD: string };
15 | readonly logger: Logger;
16 | readonly fs: {
17 | readonly readFile: typeof fs.readFile;
18 | readonly writeFile: typeof fs.writeFile;
19 | readonly mkdir: typeof fs.mkdir;
20 | };
21 | };
22 |
23 | export type Logger = {
24 | start(message: string): void;
25 | success(message: string): void;
26 | verbose(message: string): void;
27 | info(message: string): void;
28 | warn(message: string): void;
29 | error(error: Error & { cause?: unknown }): void;
30 | };
31 |
32 | export type FsInstallMethod = {
33 | readonly type: 'fs';
34 | readonly filepath: ResolvableConfigPath;
35 | readonly schema: z.Schema;
36 | readonly location: InstallMethodLocation;
37 | };
38 |
39 | export type InstallMethodLocation = 'local' | 'global';
40 |
41 | export type InstallMethod = FsInstallMethod;
42 |
43 | export type Remix = {
44 | readonly id?: string;
45 | readonly name: string;
46 | readonly target: string;
47 | };
48 |
49 | export type InstallLocation = 'local' | 'global' | 'prefer-local';
50 |
51 | export type Server = Remix;
52 |
53 | export type McpHostClient = {
54 | readonly name: string;
55 | readonly installMethods: M;
56 | install(ctx: Context, server: Server, location: InstallLocation): Promise;
57 | uninstall(ctx: Context, server: Server, location: InstallLocation): Promise;
58 | };
59 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-clients/uninstall.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs/promises';
2 |
3 | import { constants as osConstants, getPlatform as platform } from '#libs/platform';
4 |
5 | import resolveConfigPath from './config/resolve-path.ts';
6 | import { ServerNotInstalled } from './errors/index.ts';
7 | import { type IntegrationName, integrations } from './integrations/index.ts';
8 | import type { InstallLocation, Logger, Server } from './types.ts';
9 |
10 | export default async function uninstall(
11 | {
12 | cwd,
13 | logger,
14 | }: {
15 | readonly cwd: string;
16 | readonly logger: Logger;
17 | },
18 | integrationName: IntegrationName,
19 | server: Server,
20 | location: InstallLocation,
21 | ): Promise {
22 | const integration = integrations[integrationName];
23 | const serverName = JSON.stringify(server.name);
24 | logger.start(`Uninstalling ${serverName}`);
25 | try {
26 | const constants = {
27 | ...osConstants,
28 | CWD: cwd,
29 | };
30 | const { filepath } = await integration.uninstall(
31 | {
32 | platform: platform(),
33 | constants,
34 | fs,
35 | logger,
36 | },
37 | server,
38 | location,
39 | );
40 |
41 | const resolvedConfigPath = resolveConfigPath(constants, filepath);
42 | logger.success(`${serverName} was successfully uninstalled from ${JSON.stringify(resolvedConfigPath)}`);
43 | } catch (error) {
44 | if (error instanceof ServerNotInstalled) {
45 | logger.info(
46 | `${serverName} is not installed. If your server is installed globally, you need to change scope to global.`,
47 | );
48 | return;
49 | }
50 |
51 | logger.error(new Error(`Failed to uninstall ${serverName}`, { cause: error }));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as inferTargetType } from './infer-target-type.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/mcp-utils/infer-target-type.ts:
--------------------------------------------------------------------------------
1 | export default function inferTargetType(target: string) {
2 | switch (true) {
3 | case target.startsWith('ag_'):
4 | return 'agent-id' as const;
5 | case true:
6 | default:
7 | // for now everything else is OpenAPI
8 | return 'openapi' as const;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/observable/array.ts:
--------------------------------------------------------------------------------
1 | import type { Subscriber } from './types.ts';
2 |
3 | export function createShallowObservableArray(initialValues: T[]) {
4 | const array: T[] = [...initialValues];
5 | const subscribers = new Set>();
6 |
7 | return {
8 | get() {
9 | return array.slice();
10 | },
11 | get length() {
12 | return array.length;
13 | },
14 | push(...items: T[]) {
15 | array.push(...items);
16 | for (const subscriber of subscribers) {
17 | subscriber(array.slice());
18 | }
19 |
20 | return array.length;
21 | },
22 | subscribe(subscriber: Subscriber) {
23 | subscribers.add(subscriber);
24 | return () => {
25 | subscribers.delete(subscriber);
26 | };
27 | },
28 | [Symbol.dispose]() {
29 | subscribers.clear();
30 | },
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/observable/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from 'react';
2 |
3 | import type { Observable } from '../types.ts';
4 |
5 | /**
6 | * A React hook that subscribes to an observable and returns its current value
7 | */
8 | export function useObservable(observable: Observable): T {
9 | const [value, setValue] = useState(observable.get());
10 |
11 | useLayoutEffect(() => {
12 | return observable.subscribe(newValue => {
13 | setValue(newValue);
14 | });
15 | }, [observable]);
16 |
17 | return value;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/observable/index.ts:
--------------------------------------------------------------------------------
1 | export { createShallowObservableArray } from './array.ts';
2 | export { createObservableRef } from './ref.ts';
3 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/observable/ref.ts:
--------------------------------------------------------------------------------
1 | import type { Subscriber } from './types.ts';
2 |
3 | /**
4 | * Creates an observable box that notifies subscribers when its value changes
5 | */
6 | export function createObservableRef(initialValue: T) {
7 | let currentValue = initialValue;
8 | const subscribers = new Set>();
9 |
10 | return {
11 | get() {
12 | return currentValue;
13 | },
14 | set(newValue: T) {
15 | currentValue = newValue;
16 | for (const subscriber of subscribers) {
17 | subscriber(newValue);
18 | }
19 | },
20 | subscribe(subscriber: Subscriber) {
21 | subscribers.add(subscriber);
22 | return () => {
23 | subscribers.delete(subscriber);
24 | };
25 | },
26 | [Symbol.dispose]() {
27 | subscribers.clear();
28 | },
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/observable/types.ts:
--------------------------------------------------------------------------------
1 | export type Subscriber = (value: T) => void;
2 |
3 | export type Observable = {
4 | get(): T;
5 | subscribe(subscriber: Subscriber): () => void;
6 | };
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/cli.ts:
--------------------------------------------------------------------------------
1 | export { default as negotiateSecurityStrategy } from './cli/security.ts';
2 | export { default as negotiateServerUrl } from './cli/server-url.ts';
3 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/cli/server-url.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | import * as prompt from '#libs/console/prompts';
4 |
5 | import { resolveServers } from '../server/index.ts';
6 |
7 | export default async function negotiateServerUrl(service: IHttpService): Promise {
8 | const resolvedServers = resolveServers(service);
9 |
10 | if (resolvedServers.length === 0) {
11 | return prompt.url('No servers found. Please provide a server URL:');
12 | }
13 |
14 | const validServers = resolvedServers.filter(server => server.valid);
15 |
16 | if (validServers.length === 0) {
17 | return prompt.url(
18 | `We found ${service.servers?.length ?? 0} servers, but could not construct a valid URL. Please provide a server URL:`,
19 | );
20 | }
21 |
22 | if (validServers.length === 1) {
23 | return validServers[0]!.value;
24 | }
25 |
26 | const options: { label: string; hint: string; value: string }[] = [];
27 | let initial: string | undefined;
28 |
29 | for (const [i, server] of validServers.entries()) {
30 | options.push({
31 | label: server.name ?? `Server #${i + 1}`,
32 | hint: server.value,
33 | value: server.value,
34 | });
35 |
36 | if (!initial && server.name?.toLowerCase().includes('production')) {
37 | initial = server.value;
38 | }
39 | }
40 |
41 | return prompt.select({
42 | message: 'Please select a server:',
43 | initialValue: initial,
44 | options,
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/http-spec.ts:
--------------------------------------------------------------------------------
1 | import { OPERATION_CONFIG } from '@stoplight/http-spec/oas';
2 | import { transformOas2Operation, transformOas2Service } from '@stoplight/http-spec/oas2';
3 | import { transformOas3Operation, transformOas3Service } from '@stoplight/http-spec/oas3';
4 | import { isPlainObject } from '@stoplight/json';
5 | import type { IHttpOperation } from '@stoplight/types';
6 |
7 | function isOas3(document: Record) {
8 | return 'openapi' in document;
9 | }
10 |
11 | export function transformOasService(document: Record) {
12 | if (isOas3(document)) {
13 | return transformOas3Service({ document });
14 | }
15 |
16 | return transformOas2Service({ document });
17 | }
18 |
19 | export function* transformOasOperations(document: Record): Iterable {
20 | const paths = document['paths'];
21 | if (!isPlainObject(paths)) return;
22 |
23 | const verbs = new Set(['get', 'post', 'put', 'delete', 'options', 'head', 'patch']);
24 | if (isOas3(document)) {
25 | verbs.add('trace');
26 | }
27 |
28 | const transformOperation = isOas3(document) ? transformOas3Operation : transformOas2Operation;
29 | for (const [path, pathItem] of Object.entries(paths)) {
30 | if (!isPlainObject(pathItem)) continue;
31 | for (const prop of Object.keys(pathItem)) {
32 | if (!verbs.has(prop)) continue;
33 | yield {
34 | path,
35 | ...transformOperation({
36 | document,
37 | config: OPERATION_CONFIG,
38 | name: path,
39 | method: prop,
40 | }),
41 | };
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/index.ts:
--------------------------------------------------------------------------------
1 | export { negotiateSecurityStrategy, negotiateServerUrl } from './cli.ts';
2 | export { transformOasService as parseAsService } from './http-spec.ts';
3 | export { default as listTools } from './list-tools.ts';
4 | export { default as loadDocumentAsService } from './load-document-as-service.ts';
5 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/list-tools.ts:
--------------------------------------------------------------------------------
1 | import { getToolName } from '@openmcp/openapi';
2 |
3 | import { transformOasOperations } from './http-spec.ts';
4 |
5 | type ListedTool = {
6 | readonly name: string;
7 | readonly route: string;
8 | };
9 |
10 | export default function listTools(document: Record): ListedTool[] {
11 | const list: ListedTool[] = [];
12 |
13 | for (const operation of transformOasOperations(document)) {
14 | const value = getToolName(operation);
15 | list.push({
16 | name: value,
17 | route: `${operation.method.toUpperCase()} ${operation.path}`,
18 | });
19 | }
20 |
21 | return list;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/load-document-as-service.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs/promises';
2 |
3 | import { loadDocument } from '@openmcp/utils/documents';
4 |
5 | import { transformOasService } from './http-spec.ts';
6 |
7 | export type { IHttpService } from '@stoplight/types';
8 |
9 | export default async function loadDocumentAsService(location: string) {
10 | const document = await loadDocument(
11 | {
12 | fetch,
13 | fs,
14 | },
15 | location,
16 | );
17 |
18 | return transformOasService(document);
19 | }
20 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/api-key-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with API Key security schemes
4 | export const apiKeySecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'api-key-header',
9 | key: 'apiKey',
10 | type: 'apiKey',
11 | name: 'X-API-KEY',
12 | in: 'header',
13 | },
14 | ],
15 | ],
16 | securitySchemes: [
17 | {
18 | id: 'api-key-header',
19 | key: 'apiKey',
20 | type: 'apiKey',
21 | name: 'X-API-KEY',
22 | in: 'header',
23 | },
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/empty-security-array-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with empty security array
4 | export const emptySecurityArrayFragment: Pick = {
5 | security: [],
6 | securitySchemes: undefined,
7 | };
8 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/empty-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with no security
4 | export const emptySecurityFragment: Pick = {
5 | security: undefined,
6 | securitySchemes: undefined,
7 | };
8 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/http-basic-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with HTTP Basic security scheme
4 | export const httpBasicSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'basic-auth',
9 | key: 'basicAuth',
10 | type: 'http',
11 | scheme: 'basic',
12 | },
13 | ],
14 | ],
15 | securitySchemes: [
16 | {
17 | id: 'basic-auth',
18 | key: 'basicAuth',
19 | type: 'http',
20 | scheme: 'basic',
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/http-bearer-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with HTTP Bearer security scheme
4 | export const httpBearerSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'bearer-auth',
9 | key: 'bearerAuth',
10 | type: 'http',
11 | scheme: 'bearer',
12 | },
13 | ],
14 | ],
15 | securitySchemes: [
16 | {
17 | id: 'bearer-auth',
18 | key: 'bearerAuth',
19 | type: 'http',
20 | scheme: 'bearer',
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/http-bearer-with-format-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with HTTP Bearer security scheme with format
4 | export const httpBearerWithFormatSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'bearer-auth-jwt',
9 | key: 'bearerAuthJwt',
10 | type: 'http',
11 | scheme: 'bearer',
12 | bearerFormat: 'JWT',
13 | },
14 | ],
15 | ],
16 | securitySchemes: [
17 | {
18 | id: 'bearer-auth-jwt',
19 | key: 'bearerAuthJwt',
20 | type: 'http',
21 | scheme: 'bearer',
22 | bearerFormat: 'JWT',
23 | },
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/http-unsupported-scheme-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with HTTP security scheme with unsupported scheme
4 | export const httpUnsupportedSchemeFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'digest-auth',
9 | key: 'digestAuth',
10 | type: 'http',
11 | scheme: 'digest',
12 | },
13 | ],
14 | ],
15 | securitySchemes: [
16 | {
17 | id: 'digest-auth',
18 | key: 'digestAuth',
19 | type: 'http',
20 | scheme: 'digest',
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/multiple-and-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with multiple security schemes (AND relationship)
4 | export const multipleAndSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'api-key-header',
9 | key: 'apiKey',
10 | type: 'apiKey',
11 | name: 'X-API-KEY',
12 | in: 'header',
13 | },
14 | {
15 | id: 'bearer-auth',
16 | key: 'bearerAuth',
17 | type: 'http',
18 | scheme: 'bearer',
19 | },
20 | ],
21 | ],
22 | securitySchemes: [
23 | {
24 | id: 'api-key-header',
25 | key: 'apiKey',
26 | type: 'apiKey',
27 | name: 'X-API-KEY',
28 | in: 'header',
29 | },
30 | {
31 | id: 'bearer-auth',
32 | key: 'bearerAuth',
33 | type: 'http',
34 | scheme: 'bearer',
35 | },
36 | ],
37 | };
38 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/multiple-or-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with multiple security schemes (OR relationship)
4 | export const multipleOrSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'api-key-header',
9 | key: 'apiKeyHeader',
10 | type: 'apiKey',
11 | name: 'X-API-KEY',
12 | in: 'header',
13 | },
14 | ],
15 | [
16 | {
17 | id: 'bearer-auth',
18 | key: 'bearerAuth',
19 | type: 'http',
20 | scheme: 'bearer',
21 | },
22 | ],
23 | ],
24 | securitySchemes: [
25 | {
26 | id: 'api-key-header',
27 | key: 'apiKeyHeader',
28 | type: 'apiKey',
29 | name: 'X-API-KEY',
30 | in: 'header',
31 | },
32 | {
33 | id: 'bearer-auth',
34 | key: 'bearerAuth',
35 | type: 'http',
36 | scheme: 'bearer',
37 | },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/mutual-tls-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with Mutual TLS security scheme
4 | export const mutualTlsSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'mtls-auth',
9 | key: 'mtlsAuth',
10 | type: 'mutualTLS',
11 | },
12 | ],
13 | ],
14 | securitySchemes: [
15 | {
16 | id: 'mtls-auth',
17 | key: 'mtlsAuth',
18 | type: 'mutualTLS',
19 | },
20 | ],
21 | };
22 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/no-global-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with security schemes but no global security
4 | export const noGlobalSecurityFragment: Pick = {
5 | security: undefined,
6 | securitySchemes: [
7 | {
8 | id: 'api-key-header',
9 | key: 'apiKeyHeader',
10 | type: 'apiKey',
11 | name: 'X-API-KEY',
12 | in: 'header',
13 | },
14 | {
15 | id: 'api-key-query',
16 | key: 'apiKeyQuery',
17 | type: 'apiKey',
18 | name: 'api_key',
19 | in: 'query',
20 | },
21 | {
22 | id: 'api-key-cookie',
23 | key: 'apiKeyCookie',
24 | type: 'apiKey',
25 | name: 'api_key',
26 | in: 'cookie',
27 | },
28 | ],
29 | };
30 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/oauth2-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with OAuth2 security scheme
4 | export const oauth2SecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'oauth2-auth',
9 | key: 'petstoreAuth',
10 | type: 'oauth2',
11 | flows: {
12 | implicit: {
13 | authorizationUrl: 'https://example.com/oauth/authorize',
14 | scopes: {
15 | 'read:api': 'Read access to the API',
16 | 'write:api': 'Write access to the API',
17 | },
18 | },
19 | },
20 | },
21 | ],
22 | ],
23 | securitySchemes: [
24 | {
25 | id: 'oauth2-auth',
26 | key: 'petstoreAuth',
27 | type: 'oauth2',
28 | flows: {
29 | implicit: {
30 | authorizationUrl: 'https://example.com/oauth/authorize',
31 | scopes: {
32 | 'read:api': 'Read access to the API',
33 | 'write:api': 'Write access to the API',
34 | },
35 | },
36 | },
37 | },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/__tests__/fixtures/openid-connect-security-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with OpenID Connect security scheme
4 | export const openIdConnectSecurityFragment: Pick = {
5 | security: [
6 | [
7 | {
8 | id: 'openid-auth',
9 | key: 'openId',
10 | type: 'openIdConnect',
11 | openIdConnectUrl: 'https://example.com/.well-known/openid-configuration',
12 | },
13 | ],
14 | ],
15 | securitySchemes: [
16 | {
17 | id: 'openid-auth',
18 | key: 'openId',
19 | type: 'openIdConnect',
20 | openIdConnectUrl: 'https://example.com/.well-known/openid-configuration',
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/security/index.ts:
--------------------------------------------------------------------------------
1 | export { type ResolvedSecurityScheme, default as resolveSecuritySchemes } from './resolve-security-schemes.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/empty-servers-array-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with empty servers array
4 | export const emptyServersArrayFragment: Pick = {
5 | servers: [],
6 | };
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/empty-servers-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with no servers
4 | export const emptyServersFragment: Pick = {
5 | servers: undefined,
6 | };
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/invalid-templated-server-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a templated server that resolves to an invalid URL
4 | export const invalidTemplatedServerFragment: Pick = {
5 | servers: [
6 | {
7 | id: 'invalid-templated-path',
8 | url: 'https://api.example.com/{path}',
9 | description: 'API with invalid path',
10 | variables: {
11 | path: {
12 | default: 'not a valid path with spaces',
13 | },
14 | },
15 | },
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/invalid-url-server-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a server that has an invalid URL
4 | export const invalidUrlServerFragment: Pick = {
5 | servers: [{ id: 'invalid-url-server', url: 'not a valid url', description: 'Invalid URL' }],
6 | };
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/mixed-servers-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a mix of templated and non-templated servers
4 | export const mixedServersFragment: Pick = {
5 | servers: [
6 | { id: 'static-api', url: 'https://static-api.example.com', description: 'Static API' },
7 | {
8 | id: 'mixed-versioned-api',
9 | url: 'https://api.example.com/{version}',
10 | name: 'Versioned API',
11 | variables: {
12 | version: {
13 | default: 'v1',
14 | },
15 | },
16 | },
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/multiple-variables-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with multiple templated variables
4 | export const multipleVariablesFragment: Pick = {
5 | servers: [
6 | {
7 | id: 'multi-variable-api',
8 | url: 'https://{environment}.api.example.com/{version}',
9 | description: 'Multi-variable API',
10 | variables: {
11 | environment: {
12 | default: 'dev',
13 | enum: ['dev', 'staging', 'prod'],
14 | },
15 | version: {
16 | default: 'v1',
17 | enum: ['v1', 'v2'],
18 | },
19 | },
20 | },
21 | ],
22 | };
23 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/non-templated-servers-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with non-templated servers
4 | export const nonTemplatedServersFragment: Pick = {
5 | servers: [
6 | { id: 'prod-api', url: 'https://api.example.com/v1', description: 'Production API' },
7 | { id: 'dev-api', url: 'https://dev-api.example.com/v1', name: 'Development API' },
8 | { id: 'invalid-url', url: 'invalid-url', description: 'Invalid URL' },
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/server-with-name-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a server that has name but no description
4 | export const serverWithNameFragment: Pick = {
5 | servers: [{ id: 'name-only-api', url: 'https://api.example.com/v1', name: 'Production API' }],
6 | };
7 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/templated-server-with-default-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a templated server with only default value
4 | export const templatedServerWithDefaultFragment: Pick = {
5 | servers: [
6 | {
7 | id: 'versioned-api-default',
8 | url: 'https://api.example.com/{version}',
9 | name: 'Versioned API',
10 | variables: {
11 | version: {
12 | default: 'v1',
13 | },
14 | },
15 | },
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/__tests__/fixtures/templated-server-with-enum-fragment.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | // fragment with a templated server with enum values
4 | export const templatedServerWithEnumFragment: Pick = {
5 | servers: [
6 | {
7 | id: 'versioned-api-enum',
8 | url: 'https://api.example.com/{version}',
9 | description: 'API with version',
10 | variables: {
11 | version: {
12 | default: 'v1',
13 | enum: ['v1', 'v2'],
14 | },
15 | },
16 | },
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/index.ts:
--------------------------------------------------------------------------------
1 | export { type ResolvedServer, default as resolveServers } from './resolve-servers.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/resolve-servers.ts:
--------------------------------------------------------------------------------
1 | import type { IHttpService } from '@stoplight/types';
2 |
3 | import resolveTemplatedServer, { isTemplatedServer } from './resolve-templated-server.ts';
4 |
5 | export type ResolvedServer = {
6 | name: string | undefined;
7 | value: string;
8 | valid: boolean;
9 | };
10 |
11 | function toResolvedServer(name: string | undefined, url: string): ResolvedServer {
12 | return {
13 | name,
14 | value: url,
15 | valid: URL.canParse(url),
16 | };
17 | }
18 |
19 | export default function resolveServers(service: Pick): ResolvedServer[] {
20 | return (
21 | service.servers?.flatMap(server =>
22 | isTemplatedServer(server)
23 | ? resolveTemplatedServer(server).map(url => toResolvedServer(server.description ?? server.name, url))
24 | : toResolvedServer(server.description ?? server.name, server.url),
25 | ) ?? []
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/openapi/server/resolve-templated-server.ts:
--------------------------------------------------------------------------------
1 | import type { INodeVariable, IServer } from '@stoplight/types';
2 |
3 | import applyUrlVariables from './apply-url-variables.ts';
4 |
5 | export default function resolveTemplatedServer(
6 | server: IServer & { variables: Record },
7 | ): string[] {
8 | const variablePairs: [name: string, values: string[]][] = [];
9 |
10 | for (const [name, value] of Object.entries(server.variables)) {
11 | // technically default is required by spec, but we cannot be sure whether spec is valid or not
12 | if (value.default !== undefined) {
13 | variablePairs.push([name, [value.default]]);
14 | } else if (value.enum !== undefined) {
15 | variablePairs.push([name, value.enum.map(enumValue => String(enumValue))]);
16 | } else {
17 | variablePairs.push([name, []]);
18 | }
19 | }
20 |
21 | return applyUrlVariables(server.url, variablePairs);
22 | }
23 |
24 | export function isTemplatedServer(server: IServer): server is IServer & { variables: Record } {
25 | return Object.hasOwn(server, 'variables');
26 | }
27 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/platform/constants.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'node:os';
2 | import path from 'node:path';
3 | import process from 'node:process';
4 |
5 | import platform from './get-platform.ts';
6 |
7 | export const HOMEDIR = os.homedir();
8 |
9 | let configdir: string;
10 |
11 | switch (platform()) {
12 | case 'win32':
13 | // Use %APPDATA% on Windows
14 | configdir = process.env['APPDATA'] || path.join(HOMEDIR, 'AppData', 'Roaming');
15 | break;
16 | case 'darwin':
17 | // Use ~/Library/Application Support on macOS
18 | configdir = path.join(HOMEDIR, 'Library', 'Application Support');
19 | break;
20 | case 'linux':
21 | case 'unix':
22 | // Default to ~/.config on Linux/Unix
23 | configdir = process.env['XDG_CONFIG_HOME'] || path.join(HOMEDIR, '.config');
24 | }
25 |
26 | export { configdir as CONFIGDIR };
27 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/platform/get-platform.ts:
--------------------------------------------------------------------------------
1 | import { platform } from 'node:os';
2 |
3 | export default () => {
4 | const p = platform();
5 | if (p === 'win32') return 'win32';
6 | else if (p === 'darwin') return 'darwin';
7 | else if (p === 'linux') return 'linux';
8 | else return 'unix';
9 | };
10 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/platform/index.ts:
--------------------------------------------------------------------------------
1 | export * as constants from './constants.ts';
2 | export { default as getPlatform } from './get-platform.ts';
3 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/config-with-absolute-path.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {},
3 | "servers": {
4 | "test_server": {
5 | "type": "openapi",
6 | "openapi": "/placeholder/absolute/path/to/openapi.json",
7 | "serverUrl": "https://example.com",
8 | "tools": ["tool1"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/config-with-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {
3 | "test_server": { "ENV_VAR": "" }
4 | },
5 | "servers": {
6 | "test_server": {
7 | "type": "sse",
8 | "url": "https://example.com",
9 | "tools": ["tool1"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/config-with-file-url.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {},
3 | "servers": {
4 | "test_server": {
5 | "type": "openapi",
6 | "openapi": "file:///placeholder/openapi.json",
7 | "serverUrl": "https://example.com",
8 | "tools": ["tool1"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/config-with-undefined-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {
3 | "test_server": { "UNDEFINED_ENV_VAR": "" }
4 | },
5 | "servers": {
6 | "test_server": {
7 | "type": "sse",
8 | "url": "https://example.com",
9 | "tools": ["tool1"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/config-with-url.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {},
3 | "servers": {
4 | "test_server": {
5 | "type": "openapi",
6 | "openapi": "https://example.com/openapi.json",
7 | "serverUrl": "https://example.com",
8 | "tools": ["tool1"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/remote-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {
3 | "test_server": { "key": "value" }
4 | },
5 | "servers": {
6 | "test_server": {
7 | "type": "sse",
8 | "url": "https://example.com",
9 | "tools": ["tool1"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/__tests__/fixtures/valid-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": {
3 | "test_server": { "key": "value" }
4 | },
5 | "servers": {
6 | "test_server": {
7 | "type": "sse",
8 | "url": "https://example.com",
9 | "tools": ["tool1"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/index.ts:
--------------------------------------------------------------------------------
1 | export { default as load } from './load.ts';
2 | export { default as parse } from './parse.ts';
3 | export type {
4 | Config,
5 | OpenAPIServer,
6 | RemixServer,
7 | SSEServer,
8 | StdIOServer,
9 | StreamableHTTPServer,
10 | Tool,
11 | } from './schemas.ts';
12 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/config/parse.ts:
--------------------------------------------------------------------------------
1 | import { type Config, ConfigSchema } from './schemas.ts';
2 |
3 | export default function parseConfig(config: unknown): Config {
4 | const res = ConfigSchema.safeParse(config);
5 | if (!res.success) {
6 | throw new Error(
7 | `Invalid config: ${res.error.issues.map(issue => `${issue.path.join('.')} - ${issue.message}`).join(', ')}`,
8 | );
9 | }
10 |
11 | return res.data;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/errors.ts:
--------------------------------------------------------------------------------
1 | export class ServerRegistrationError extends Error {
2 | override readonly name = 'ServerRegistrationError';
3 |
4 | constructor(name: string, reason: unknown) {
5 | super(`Failed to register server: ${name}`, {
6 | cause: reason,
7 | });
8 | }
9 | }
10 |
11 | export class ClientServerRegistrationError extends Error {
12 | override readonly name = 'ClientServerRegistrationError';
13 |
14 | constructor(name: string, reason: unknown) {
15 | super(`Failed to register client server: ${name}`, {
16 | cause: reason,
17 | });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | Config,
3 | OpenAPIServer,
4 | RemixServer,
5 | SSEServer,
6 | StdIOServer,
7 | StreamableHTTPServer,
8 | } from './config/index.ts';
9 | export { load as loadConfig, parse as parseConfig } from './config/index.ts';
10 | export { default as createRemixServer } from './server.ts';
11 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/manager/client-servers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as registerClientServers } from './register-client-servers.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/manager/client-servers/scoped-client-server.ts:
--------------------------------------------------------------------------------
1 | import { ClientServer, type ClientServerOptions, type ClientServerStorageData, type Tool } from '@openmcp/manager';
2 |
3 | import { resolveToolName } from '../../utils/tools.ts';
4 |
5 | export default class ScopedClientServer extends ClientServer {
6 | readonly #allowedTools: Set;
7 |
8 | constructor(data: ClientServerStorageData, options: ClientServerOptions, allowedTools: Set) {
9 | super(data, options);
10 | this.#allowedTools = allowedTools;
11 | }
12 |
13 | override async listTools(): Promise {
14 | const tools = await super.listTools({ lazyConnect: true });
15 | const exposedToolNames = new Set();
16 | const exposedTools: Tool[] = [];
17 | for (const tool of tools) {
18 | if (this.#allowedTools.size !== 0 && !this.#allowedTools.has(tool.name)) continue;
19 |
20 | const resolvedToolName = resolveToolName(this.serverId, tool.name);
21 | if (exposedToolNames.has(tool.name)) {
22 | // todo: we could try to handle it by renaming the tool
23 | throw new Error(`Tool name collision: ${tool.name} and ${resolvedToolName}`);
24 | }
25 |
26 | exposedToolNames.add(resolvedToolName);
27 | exposedTools.push({
28 | ...tool,
29 | name: resolvedToolName,
30 | });
31 | }
32 |
33 | return exposedTools;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/manager/servers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as registerServers } from './register-servers.ts';
2 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/manager/servers/interpolate-openapi-client-config.ts:
--------------------------------------------------------------------------------
1 | import { type ClientConfig } from '@openmcp/openapi';
2 |
3 | import strictReplaceVariables from '../../utils/strict-replace-variables.ts';
4 |
5 | export default function interpolateOpenAPIClientConfig(
6 | config: Record,
7 | userConfig: unknown,
8 | ): ClientConfig {
9 | if (!config) {
10 | return {};
11 | }
12 |
13 | const interpolatedConfig: ClientConfig = {};
14 | for (const [key, value] of Object.entries(config)) {
15 | if (typeof value === 'object' && value !== null) {
16 | interpolatedConfig[key] = Object.entries(value).reduce(
17 | (acc, [k, v]) => {
18 | acc[k] = typeof v === 'string' ? strictReplaceVariables(v, userConfig) : v;
19 | return acc;
20 | },
21 | {} as Record,
22 | );
23 | }
24 | }
25 |
26 | return interpolatedConfig;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/manager/servers/to-transport-config.ts:
--------------------------------------------------------------------------------
1 | import type { TransportConfig } from '@openmcp/manager';
2 |
3 | import type { RemixServer, SSEServer } from '../../config/index.ts';
4 | import type { StreamableHTTPServer } from '../../config/schemas.ts';
5 | import strictReplaceVariables from '../../utils/strict-replace-variables.ts';
6 |
7 | function getHttpConfig(
8 | { url, headers }: StreamableHTTPServer | SSEServer,
9 | userConfig: unknown,
10 | ): TransportConfig['config'] {
11 | return {
12 | url: strictReplaceVariables(url, userConfig),
13 | requestInit: {
14 | headers: headers
15 | ? Object.entries(headers).reduce(
16 | (acc, [key, value]) => {
17 | acc[key] = strictReplaceVariables(value, userConfig);
18 | return acc;
19 | },
20 | {} as Record,
21 | )
22 | : undefined,
23 | },
24 | };
25 | }
26 |
27 | export default function toTransportConfig(server: RemixServer, userConfig: unknown): TransportConfig {
28 | switch (server.type) {
29 | case 'stdio':
30 | return {
31 | type: 'stdio',
32 | config: {
33 | command: strictReplaceVariables(server.command, userConfig),
34 | args: server.args.map(arg => strictReplaceVariables(arg, userConfig)),
35 | },
36 | };
37 | case 'streamable-http':
38 | return {
39 | type: 'streamableHttp',
40 | config: getHttpConfig(server, userConfig),
41 | };
42 | case 'sse':
43 | return {
44 | type: 'sse',
45 | config: getHttpConfig(server, userConfig),
46 | };
47 | case 'openapi':
48 | return {
49 | type: 'inMemory',
50 | config: {},
51 | };
52 | default:
53 | throw new Error(`Unsupported transport type: ${server['type']}`);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/utils/__tests__/strict-replace-variables.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 |
3 | import strictReplaceVariables from '../strict-replace-variables.ts';
4 |
5 | describe('strictReplaceVariables', () => {
6 | it('should replace variables when all are provided', () => {
7 | const input = 'Hello, {{name}}!';
8 | const values = { name: 'World' };
9 | const result = strictReplaceVariables(input, values);
10 | expect(result).toBe('Hello, World!');
11 | });
12 |
13 | it('should throw an error when a variable is missing', () => {
14 | const input = 'Hello, {{name}}!';
15 | const values = {};
16 | expect(() => strictReplaceVariables(input, values)).toThrow('Missing variable: name');
17 | });
18 |
19 | it('should handle inputs with no variables', () => {
20 | const input = 'Hello, World!';
21 | const values = { name: 'Unused' };
22 | const result = strictReplaceVariables(input, values);
23 | expect(result).toBe('Hello, World!');
24 | });
25 |
26 | it('should ignore non-object values for the variables parameter', () => {
27 | const input = 'Hello, {{name}}!';
28 | const values = null;
29 | expect(() => strictReplaceVariables(input, values)).toThrow('Missing variable: name');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/utils/strict-replace-variables.ts:
--------------------------------------------------------------------------------
1 | import { replaceVariables } from '@openmcp/utils';
2 |
3 | function isPlainObject(value: unknown): value is Record {
4 | return typeof value === 'object' && value !== null && !Array.isArray(value);
5 | }
6 |
7 | export default function strictReplaceVariables(input: string, values: unknown): string {
8 | const obj = isPlainObject(values) ? values : {};
9 | return replaceVariables(
10 | input,
11 | new Proxy(obj, {
12 | get(target, p, recv) {
13 | if (typeof p === 'symbol') {
14 | return Reflect.get(target, p, recv);
15 | }
16 |
17 | if (!Reflect.has(target, p)) {
18 | throw new Error(`Missing variable: ${String(p)}`);
19 | }
20 |
21 | return Reflect.get(target, p, recv);
22 | },
23 | }),
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/remix/utils/tools.ts:
--------------------------------------------------------------------------------
1 | import type { ToolName } from '@openmcp/server';
2 |
3 | const DELIMITER = '_';
4 |
5 | export function parseToolName(name: ToolName): [serverId: string, toolName: ToolName] {
6 | const delimIndex = name.indexOf(DELIMITER);
7 | if (delimIndex === -1) {
8 | throw new Error(`Invalid tool name: ${name}`);
9 | }
10 |
11 | return [name.slice(0, delimIndex), name.slice(delimIndex + 2)];
12 | }
13 |
14 | export function resolveToolName(serverId: string, name: ToolName): ToolName {
15 | const toolName = [serverId, name].join(DELIMITER);
16 | return toolName.length > 64 ? toolName.slice(0, 64) : toolName;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/src/libs/string-utils/index.ts:
--------------------------------------------------------------------------------
1 | import snakeCase from 'lodash-es/snakeCase.js';
2 |
3 | export { default as slugify } from '@sindresorhus/slugify';
4 |
5 | export function interpolable(value: string): string {
6 | return `{{${value}}}`;
7 | }
8 |
9 | export const screamCase = (value: string) => snakeCase(value).toUpperCase();
10 | export { default as camelCase } from 'lodash-es/camelCase.js';
11 |
--------------------------------------------------------------------------------
/packages/cli/src/register.ts:
--------------------------------------------------------------------------------
1 | import { default as yargs } from 'yargs';
2 | import { hideBin } from 'yargs/helpers';
3 |
4 | import console from '#libs/console';
5 |
6 | import pkgJson from '../package.json' with { type: 'json' };
7 | import installCommand from './commands/install/index.ts';
8 | import loginCommand from './commands/login/index.ts';
9 | import logoutCommand from './commands/logout/index.ts';
10 | import runCommand from './commands/run/index.ts';
11 | import uninstallCommand from './commands/uninstall/index.ts';
12 | import uploadCommand from './commands/upload/index.ts';
13 | import whoamiCommand from './commands/whoami/index.ts';
14 | import { HandlerError } from './errors/index.ts';
15 |
16 | export default async function register(argv: string[]) {
17 | try {
18 | await yargs(hideBin(process.argv))
19 | .scriptName('openmcp')
20 | .version(pkgJson.version)
21 | .help(true)
22 | .fail((msg, err, yargs) => {
23 | if (err instanceof HandlerError) {
24 | console.error(err.message);
25 | } else {
26 | console.restoreAll();
27 | if (msg !== null) {
28 | process.stderr.write(String(msg) + '\n');
29 | }
30 | yargs.showHelp();
31 | process.exit(1);
32 | }
33 | })
34 | .wrap(yargs().terminalWidth())
35 | .strictCommands()
36 | .command(loginCommand)
37 | .command(logoutCommand)
38 | .command(whoamiCommand)
39 | .command(installCommand)
40 | .command(uninstallCommand)
41 | .command(runCommand)
42 | .command(uploadCommand)
43 | .demandCommand(1, '')
44 | .parse(argv);
45 | } catch {
46 | process.exit(1);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/cli/src/rpc/agents.ts:
--------------------------------------------------------------------------------
1 | import { type } from '@orpc/contract';
2 | import { z } from 'zod';
3 |
4 | import type { Config as RemixDefinition } from '#libs/remix';
5 |
6 | import { base } from './base.ts';
7 |
8 | export type Agent = {
9 | id: string;
10 | name: string;
11 | };
12 |
13 | const listAgentsContract = base
14 | .input(z.object({ name: z.string().optional() }))
15 | .output(type())
16 | .errors({ NOT_FOUND: {} });
17 |
18 | const getAgentContract = base
19 | .input(z.object({ agentId: z.string() }))
20 | .output(type())
21 | .errors({ NOT_FOUND: {} });
22 |
23 | const getRemixContract = base
24 | .input(z.object({ agentId: z.string() }))
25 | .output(type())
26 | .errors({ NOT_FOUND: {} });
27 |
28 | export const agentsRouterContract = {
29 | agents: base.router({
30 | listAgents: listAgentsContract,
31 | getAgent: getAgentContract,
32 | getRemix: getRemixContract,
33 | }),
34 | };
35 |
--------------------------------------------------------------------------------
/packages/cli/src/rpc/base.ts:
--------------------------------------------------------------------------------
1 | import { oc, type } from '@orpc/contract';
2 |
3 | export const base = oc.errors({
4 | UNAUTHORIZED: {},
5 | INPUT_VALIDATION_FAILED: {
6 | status: 422,
7 | message: 'Input validation failed',
8 | data: type<{
9 | formErrors: string[];
10 | fieldErrors: Record;
11 | }>(),
12 | },
13 | BAD_REQUEST: {},
14 | });
15 |
--------------------------------------------------------------------------------
/packages/cli/src/rpc/index.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ContractRouterClient as BaseContractRouterClient,
3 | InferContractRouterInputs,
4 | InferContractRouterOutputs,
5 | } from '@orpc/contract';
6 |
7 | import { agentsRouterContract } from './agents.ts';
8 | import { mcpServersRouterContract } from './mcp-servers.ts';
9 |
10 | export const routerContract = {
11 | cli: {
12 | ...agentsRouterContract,
13 | ...mcpServersRouterContract,
14 | },
15 | };
16 |
17 | export type RouterInputs = InferContractRouterInputs;
18 |
19 | export type RouterOutputs = InferContractRouterOutputs;
20 |
21 | export type ContractRouterClient = BaseContractRouterClient;
22 |
--------------------------------------------------------------------------------
/packages/cli/src/rpc/mcp-servers.ts:
--------------------------------------------------------------------------------
1 | import { TransportSchema } from '@openmcp/schemas/mcp';
2 | import { type } from '@orpc/contract';
3 | import { z } from 'zod';
4 |
5 | import { base } from './base.ts';
6 | import { McpClientConfigSchemaSchema, ToolInputSchemaSchema, ToolOutputSchemaSchema } from './schemas.ts';
7 |
8 | const uploadContract = base
9 | .input(
10 | z.object({
11 | name: z.string(),
12 | externalId: z.string().min(2).max(255),
13 | summary: z.string().optional(),
14 | description: z.string().optional(),
15 | instructions: z.string().optional(),
16 | iconUrl: z.string().url().optional(),
17 | developer: z.string().optional(),
18 | developerUrl: z.string().url().optional(),
19 | sourceUrl: z.string().url().optional(),
20 | configSchema: McpClientConfigSchemaSchema.optional(),
21 | transport: TransportSchema,
22 | visibility: z.enum(['public', 'private']).optional(),
23 | tools: z
24 | .array(
25 | z.object({
26 | name: z.string(),
27 | displayName: z.string().optional(),
28 | summary: z.string().optional(),
29 | description: z.string().optional(),
30 | instructions: z.string().optional(),
31 | inputSchema: ToolInputSchemaSchema.optional(),
32 | outputSchema: ToolOutputSchemaSchema.optional(),
33 | isReadonly: z.boolean().optional(),
34 | isDestructive: z.boolean().optional(),
35 | isIdempotent: z.boolean().optional(),
36 | isOpenWorld: z.boolean().optional(),
37 | }),
38 | )
39 | .default([]),
40 | }),
41 | )
42 | .output(type<{ id: string }>());
43 |
44 | export const mcpServersRouterContract = {
45 | mcpServers: {
46 | upload: uploadContract,
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/packages/cli/src/rpc/schemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const McpClientConfigSchemaSchema = z.object({
4 | type: z.literal('object').optional(),
5 | properties: z
6 | .record(
7 | z.object({
8 | type: z.enum(['string', 'number', 'boolean']),
9 | title: z.string().optional(),
10 | description: z.string().optional(),
11 | default: z.union([z.string(), z.number(), z.boolean()]).optional(),
12 | enum: z.array(z.string()).optional(),
13 | format: z.union([z.literal('secret'), z.string()]).optional(),
14 | example: z.union([z.string(), z.number(), z.boolean()]).optional(),
15 | }),
16 | )
17 | .optional(),
18 | required: z.array(z.string()).optional(),
19 | });
20 |
21 | export const ToolInputSchemaSchema = z
22 | .object({
23 | type: z.literal('object').optional(),
24 | properties: z.optional(z.object({}).passthrough()),
25 | })
26 | .passthrough();
27 |
28 | export const ToolOutputSchemaSchema = z
29 | .object({
30 | type: z.string().optional(),
31 | properties: z.optional(z.object({}).passthrough()),
32 | })
33 | .passthrough();
34 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/app.tsx:
--------------------------------------------------------------------------------
1 | import { Box, useInput } from 'ink';
2 | import React from 'react';
3 |
4 | import { OperationCanceledError } from '#errors';
5 | import { state } from '#libs/console/prompts';
6 | import { useObservable } from '#libs/observable/hooks';
7 |
8 | import Logs from './logs.tsx';
9 | import Prompt from './prompt.tsx';
10 |
11 | interface AppProps {
12 | onCancel(): void;
13 | }
14 |
15 | const App = React.memo(({ onCancel }: AppProps) => {
16 | const currentPrompt = useObservable(state.currentPrompt);
17 | useInput((input, key) => {
18 | if (input !== 'c' || !key.ctrl) {
19 | return;
20 | }
21 |
22 | if (currentPrompt) {
23 | currentPrompt?.reject(new OperationCanceledError());
24 | } else {
25 | onCancel();
26 | }
27 | });
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | );
35 | });
36 |
37 | export default App;
38 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/components/input-label.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from 'ink';
2 | import React from 'react';
3 |
4 | import TextRow from './text-row.tsx';
5 |
6 | interface InputLabelProps {
7 | /**
8 | * The label text to display
9 | */
10 | label: string;
11 |
12 | /**
13 | * Optional hint text to display below the label
14 | */
15 | hint?: string;
16 | }
17 |
18 | /**
19 | * Component for rendering a consistent label and hint for input components
20 | */
21 | const InputLabel = React.memo(({ label, hint }) => {
22 | return (
23 |
24 |
25 | {hint ? (
26 |
27 |
28 | {hint}
29 |
30 |
31 | ) : null}
32 |
33 | );
34 | });
35 |
36 | export default InputLabel;
37 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/components/text-row.tsx:
--------------------------------------------------------------------------------
1 | import type { ColorName } from 'chalk';
2 | import { Box, Text } from 'ink';
3 | import React from 'react';
4 |
5 | interface TextRowProps {
6 | icon: string;
7 | color: ColorName;
8 | value: string;
9 | }
10 |
11 | const TextRow = React.memo(({ icon, color, value }) => {
12 | return (
13 |
14 |
15 | {icon}
16 |
17 |
18 |
19 | {value}
20 |
21 |
22 |
23 | );
24 | });
25 |
26 | export default TextRow;
27 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/index.ts:
--------------------------------------------------------------------------------
1 | import { render as inkRender } from 'ink';
2 | import React from 'react';
3 |
4 | import console from '#libs/console';
5 |
6 | import App from './app.tsx';
7 |
8 | export function render() {
9 | const node = React.createElement(App, {
10 | onCancel() {
11 | console.error('Operation canceled');
12 | instance.rerender(node);
13 | process.exit(1);
14 | },
15 | });
16 | const instance = inkRender(node, {
17 | exitOnCtrlC: false,
18 | });
19 | return {
20 | unmount: instance.unmount.bind(instance),
21 | rerender() {
22 | instance.rerender(node);
23 | },
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/confirm.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text, useInput } from 'ink';
2 | import React, { useState } from 'react';
3 |
4 | interface IConfirmInputProps {
5 | label: string;
6 | defaultValue?: boolean;
7 | onSubmit(value: boolean): void;
8 | }
9 |
10 | const ConfirmInput = React.memo(({ defaultValue = true, label, onSubmit }) => {
11 | const [value, setValue] = useState(defaultValue);
12 |
13 | useInput((input, key) => {
14 | if (input === 'y' || input === 'Y') {
15 | setValue(true);
16 | } else if (input === 'n' || input === 'N') {
17 | setValue(false);
18 | } else if (key.return) {
19 | onSubmit(value);
20 | }
21 | });
22 |
23 | return (
24 |
25 | {value ? '✅' : '❌'}
26 |
27 | {label}
28 |
29 | {value ? '(Y/n)' : '(y/N)'}
30 |
31 | );
32 | });
33 |
34 | export default ConfirmInput;
35 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ConfirmInput } from './confirm.tsx';
2 | export { MultiSelectInput, SelectInput } from './selects/index.ts';
3 | export { default as TextInput } from './text.tsx';
4 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/components/option-item.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from 'ink';
2 | import React from 'react';
3 |
4 | interface OptionItemProps {
5 | /**
6 | * The label to display
7 | */
8 | label: string;
9 |
10 | /**
11 | * Whether this option is currently highlighted
12 | */
13 | isHighlighted: boolean;
14 |
15 | /**
16 | * Whether this option is currently selected (for multi-select)
17 | */
18 | isSelected?: boolean;
19 |
20 | /**
21 | * Whether to show checkbox (for multi-select)
22 | */
23 | showCheckbox?: boolean;
24 | }
25 |
26 | /**
27 | * Component for rendering a single option item in a select list
28 | */
29 | const OptionItem = React.memo(({ label, isHighlighted, isSelected, showCheckbox = false }) => {
30 | const checkbox = showCheckbox ? (isSelected ? '✅' : '⬜') : null;
31 |
32 | return (
33 |
34 | {checkbox === null ? null : {`${checkbox} `}}
35 |
40 | {label}
41 |
42 |
43 | );
44 | });
45 |
46 | export default OptionItem;
47 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/components/search-display.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from 'ink';
2 | import React from 'react';
3 |
4 | interface SearchDisplayProps {
5 | /**
6 | * The current search query
7 | */
8 | searchQuery: string;
9 | }
10 |
11 | /**
12 | * Component for displaying the current search query
13 | */
14 | const SearchDisplay = React.memo(({ searchQuery }) => {
15 | if (searchQuery.length === 0) {
16 | return null;
17 | }
18 |
19 | return (
20 |
21 | Search:
22 | {searchQuery}
23 |
24 | );
25 | });
26 |
27 | export default SearchDisplay;
28 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/hooks/useFilteredOptions.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import type { Option } from '../types.ts';
4 |
5 | /**
6 | * Hook to filter options based on a search query
7 | * @param options The list of options to filter
8 | * @param searchQuery The search query to filter by
9 | * @returns The filtered list of options
10 | */
11 | export default function useFilteredOptions(options: Option[], searchQuery: string): Option[] {
12 | return useMemo(() => {
13 | if (searchQuery.length === 0) {
14 | return options;
15 | }
16 |
17 | return options.filter(option => {
18 | const query = searchQuery.toLowerCase();
19 | return option.label.toLowerCase().includes(query) || option.hint?.toLowerCase().includes(query);
20 | });
21 | }, [searchQuery, options]);
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/hooks/useMultiSelectInput.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import type { Option } from '../types.ts';
4 | import useSelectInputBase from './useSelectInputBase.ts';
5 |
6 | interface UseMultiSelectInputOptions {
7 | /**
8 | * The list of options to select from
9 | */
10 | options: Option[];
11 |
12 | /**
13 | * The initial selected values
14 | * @default []
15 | */
16 | defaultValues?: string[];
17 |
18 | /**
19 | * Callback when values are submitted
20 | */
21 | onSubmit(values: string[]): void;
22 | }
23 |
24 | /**
25 | * Hook to handle input for multi-select components
26 | */
27 | function useMultiSelectInput({ options, defaultValues = [], onSubmit }: UseMultiSelectInputOptions) {
28 | const result = useSelectInputBase({
29 | options,
30 | multiple: true,
31 | defaultValues,
32 | onSubmit,
33 | });
34 |
35 | return useMemo(
36 | () => ({
37 | searchQuery: result.searchQuery,
38 | filteredOptions: result.filteredOptions,
39 | highlightedIndex: result.highlightedIndex,
40 | selectedValues: result.selectedValues,
41 | allSelected: result.allSelected,
42 | showAllToggle: result.showAllToggle,
43 | }),
44 | [result],
45 | );
46 | }
47 |
48 | export default useMultiSelectInput;
49 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/hooks/useSearchInput.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from 'react';
2 |
3 | /**
4 | * Hook to manage search input functionality
5 | * @param initialQuery Initial search query
6 | * @returns Object with search query and functions to update it
7 | */
8 | function useSearchInput(initialQuery: string = '') {
9 | const [searchQuery, setSearchQuery] = useState(initialQuery);
10 |
11 | const addCharacter = useCallback((char: string) => {
12 | setSearchQuery(prev => prev + char);
13 | }, []);
14 |
15 | const removeCharacter = useCallback(() => {
16 | setSearchQuery(prev => prev.slice(0, -1));
17 | }, []);
18 |
19 | const clearQuery = useCallback(() => {
20 | setSearchQuery('');
21 | }, []);
22 |
23 | return useMemo(
24 | () => ({
25 | searchQuery,
26 | setSearchQuery,
27 | addCharacter,
28 | removeCharacter,
29 | clearQuery,
30 | }),
31 | [searchQuery, setSearchQuery, addCharacter, removeCharacter, clearQuery],
32 | );
33 | }
34 |
35 | export default useSearchInput;
36 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/hooks/useSelectInput.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import type { Option } from '../types.ts';
4 | import useSelectInputBase from './useSelectInputBase.ts';
5 |
6 | interface UseSelectInputOptions {
7 | /**
8 | * The list of options to select from
9 | */
10 | options: Option[];
11 |
12 | /**
13 | * The initial index to highlight
14 | * @default 0
15 | */
16 | initialHighlightedIndex?: number;
17 |
18 | /**
19 | * The minimum index value (can be negative for special items like "All" toggle)
20 | * @default 0
21 | */
22 | minIndex?: number;
23 |
24 | /**
25 | * Default value for the search input
26 | * @default ''
27 | */
28 | defaultValue?: string;
29 |
30 | /**
31 | * Callback when an item is submitted
32 | */
33 | onSubmit(value: string): void;
34 | }
35 |
36 | /**
37 | * Hook to handle input for select components
38 | */
39 | export default function useSelectInput({
40 | options,
41 | initialHighlightedIndex = 0,
42 | minIndex = 0,
43 | defaultValue = '',
44 | onSubmit,
45 | }: UseSelectInputOptions) {
46 | const result = useSelectInputBase({
47 | options,
48 | multiple: false,
49 | initialHighlightedIndex,
50 | minIndex,
51 | defaultValues: defaultValue ? [defaultValue] : [],
52 | onSubmit([value]) {
53 | onSubmit(value);
54 | },
55 | });
56 |
57 | return useMemo(
58 | () => ({
59 | searchQuery: result.searchQuery,
60 | filteredOptions: result.filteredOptions,
61 | highlightedIndex: result.highlightedIndex,
62 | setHighlightedIndex: result.setHighlightedIndex,
63 | }),
64 | [result],
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MultiSelectInput } from './multi-select.tsx';
2 | export { default as SelectInput } from './select.tsx';
3 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/multi-select.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from 'ink';
2 | import React from 'react';
3 |
4 | import InputLabel from '../../components/input-label.tsx';
5 | import OptionsList from './components/options-list.tsx';
6 | import SearchDisplay from './components/search-display.tsx';
7 | import useMultiSelectInput from './hooks/useMultiSelectInput.ts';
8 | import type { Option } from './types.ts';
9 |
10 | interface IMultiSelectInputProps {
11 | label: string;
12 | options: Option[];
13 | defaultValues?: string[];
14 | optional?: boolean;
15 | onSubmit(values: string[]): void;
16 | /**
17 | * Maximum number of items to show at once (enables scrolling)
18 | * @defaultValue 10
19 | */
20 | maxVisibleItems?: number;
21 | }
22 |
23 | const MultiSelectInput = React.memo(
24 | ({ label, options, defaultValues = [], optional = false, onSubmit, maxVisibleItems = 5 }) => {
25 | const { searchQuery, filteredOptions, highlightedIndex, selectedValues, allSelected, showAllToggle } =
26 | useMultiSelectInput({
27 | options,
28 | defaultValues,
29 | onSubmit,
30 | });
31 |
32 | return (
33 |
34 |
38 |
39 |
40 |
48 |
49 |
50 | );
51 | },
52 | );
53 |
54 | export default MultiSelectInput;
55 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/select.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from 'ink';
2 | import React from 'react';
3 |
4 | import InputLabel from '../../components/input-label.tsx';
5 | import OptionsList from './components/options-list.tsx';
6 | import SearchDisplay from './components/search-display.tsx';
7 | import useSelectInput from './hooks/useSelectInput.ts';
8 | import type { Option } from './types.ts';
9 |
10 | interface ISelectInputProps {
11 | label: string;
12 | options: Option[];
13 | defaultValue?: string;
14 | onSubmit(value: string): void;
15 | /**
16 | * Maximum number of items to show at once (enables scrolling)
17 | * @defaultValue 10
18 | */
19 | maxVisibleItems?: number;
20 | }
21 |
22 | const SelectInput = React.memo(
23 | ({ label, options, defaultValue = '', onSubmit, maxVisibleItems = 5 }) => {
24 | const { searchQuery, filteredOptions, highlightedIndex } = useSelectInput({
25 | options,
26 | defaultValue,
27 | onSubmit,
28 | });
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
40 |
41 |
42 | );
43 | },
44 | );
45 |
46 | export default SelectInput;
47 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/input/selects/types.ts:
--------------------------------------------------------------------------------
1 | export interface Option {
2 | label: string;
3 | value: string;
4 | hint?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/logs.tsx:
--------------------------------------------------------------------------------
1 | import type { ColorName } from 'chalk';
2 | import { Box } from 'ink';
3 | import React from 'react';
4 |
5 | import { logs, type LogType } from '#libs/console/reporters/fancy';
6 | import { useObservable } from '#libs/observable/hooks';
7 |
8 | import TextRow from './components/text-row.tsx';
9 |
10 | const COLORS_MAP = {
11 | success: 'green',
12 | error: 'red',
13 | info: 'blue',
14 | start: 'cyanBright',
15 | warn: 'yellow',
16 | verbose: 'gray',
17 | } as const satisfies Record;
18 |
19 | const ICONS_MAP = {
20 | success: '🌟',
21 | error: '💥',
22 | info: '💡',
23 | start: '🚀',
24 | warn: '⚠️',
25 | verbose: '📝',
26 | } as const satisfies Record;
27 |
28 | const Logs = React.memo(() => {
29 | const logEntries = useObservable(logs);
30 | return (
31 |
32 | {logEntries.map(log => (
33 |
34 | ))}
35 |
36 | );
37 | });
38 |
39 | export default Logs;
40 |
--------------------------------------------------------------------------------
/packages/cli/src/ui/prompt.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { state } from '#libs/console/prompts';
4 | import { useObservable } from '#libs/observable/hooks';
5 |
6 | import { ConfirmInput, MultiSelectInput, SelectInput, TextInput } from './input/index.ts';
7 |
8 | const Prompt = React.memo(() => {
9 | const currentPrompt = useObservable(state.currentPrompt);
10 | switch (currentPrompt?.type) {
11 | case 'text':
12 | return (
13 |
22 | );
23 | case 'confirm':
24 | return (
25 |
30 | );
31 | case 'select':
32 | return (
33 |
39 | );
40 | case 'multi-select':
41 | return (
42 |
48 | );
49 | case undefined:
50 | return null;
51 | }
52 | });
53 |
54 | export default Prompt;
55 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.node.json",
3 | "include": ["**/*.ts", "**/*.tsx"],
4 | "compilerOptions": {
5 | "jsx": "react",
6 | "rootDir": "."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cli/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: 'node',
6 | include: ['src/**/*.spec.ts'],
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/packages/manager/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openmcp/manager
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371) Thanks
8 | [@P0lip](https://github.com/P0lip)! - Initial release
9 |
--------------------------------------------------------------------------------
/packages/manager/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default baseConfig;
4 |
--------------------------------------------------------------------------------
/packages/manager/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openmcp/manager",
3 | "version": "0.0.3",
4 | "description": "Manage connections between MCP Servers and Clients through a single interface.",
5 | "type": "module",
6 | "license": "MIT",
7 | "sideEffects": false,
8 | "keywords": [
9 | "mcp",
10 | "model context protocol"
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/getdatanaut/openmcp",
15 | "directory": "packages/manager"
16 | },
17 | "files": [
18 | "dist",
19 | "src",
20 | "README.md",
21 | "CHANGELOG.md",
22 | "LICENSE",
23 | "package.json"
24 | ],
25 | "exports": {
26 | ".": {
27 | "types": "./src/index.ts",
28 | "development": "./src/index.ts",
29 | "browser": "./dist/browser/index.js",
30 | "default": "./dist/node/index.js"
31 | }
32 | },
33 | "scripts": {
34 | "build": "tsup",
35 | "lint": "eslint .",
36 | "test": "vitest run",
37 | "test.watch": "vitest watch",
38 | "typecheck": "tsc"
39 | },
40 | "dependencies": {
41 | "@modelcontextprotocol/sdk": "^1.11.0"
42 | },
43 | "devDependencies": {
44 | "@datanaut/eslint-config": "0.1.1",
45 | "@datanaut/tsconfig": "0.1.3",
46 | "eslint": "9.26.0",
47 | "tsup": "8.4.0",
48 | "type-fest": "4.41.0",
49 | "typescript": "5.8.3",
50 | "vitest": "3.1.3"
51 | },
52 | "publishConfig": {
53 | "access": "public"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/manager/src/index.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface ImportMetaEnv {
3 | readonly PLATFORM: 'node' | 'browser';
4 | }
5 |
6 | interface ImportMeta {
7 | readonly env: ImportMetaEnv;
8 | }
9 | }
10 |
11 | export type { ClientServerId, ClientServerOptions, ClientServerStorageData, Tool } from './client-servers.ts';
12 | export { ClientServer, ClientServerManager } from './client-servers.ts';
13 | export type { McpManagerOptions, McpManagerStorage } from './manager.ts';
14 | export { createMcpManager, McpManager } from './manager.ts';
15 | export type { ServerOptions, ServerStorageData } from './servers.ts';
16 | export { Server } from './servers.ts';
17 | export type { Storage } from './storage/index.ts';
18 | export type { TransportConfig, TransportConfigs } from './transport.ts';
19 | export type { ClientId, McpManagerId, ServerId } from './types.ts';
20 |
--------------------------------------------------------------------------------
/packages/manager/src/manager.ts:
--------------------------------------------------------------------------------
1 | import { type ClientServerManager, type ClientServerStorageData, createClientServerManager } from './client-servers.ts';
2 | import { createServerManager, type ServerManager, type ServerStorageData } from './servers.ts';
3 | import type { Storage } from './storage/index.ts';
4 | import { createMemoryStorage } from './storage/memory.ts';
5 | import type { McpManagerId } from './types.ts';
6 |
7 | export interface McpManagerOptions {
8 | /**
9 | * A unique identifier for this manager
10 | */
11 | id?: McpManagerId;
12 |
13 | /**
14 | * Optionally provide a Storage implementation to persist manager state
15 | *
16 | * @default MemoryStorage
17 | */
18 | storage?: Partial;
19 | }
20 |
21 | export interface McpManagerStorage {
22 | servers: Storage;
23 | clientServers: Storage;
24 | }
25 |
26 | export function createMcpManager(options?: McpManagerOptions) {
27 | return new McpManager(options);
28 | }
29 |
30 | /**
31 | * The McpManager maintains knowledge of registered servers,
32 | * connected clients, server<->client connections.
33 | */
34 | export class McpManager {
35 | public readonly id: McpManagerId;
36 | public readonly servers: ServerManager;
37 | public readonly clientServers: ClientServerManager;
38 | public readonly storage: McpManagerStorage;
39 |
40 | constructor({ id, storage }: McpManagerOptions = {}) {
41 | this.id = id ?? 'no-id';
42 | this.storage = {
43 | servers: storage?.servers ?? createMemoryStorage(),
44 | clientServers: storage?.clientServers ?? createMemoryStorage(),
45 | };
46 | this.servers = createServerManager({ manager: this });
47 | this.clientServers = createClientServerManager({ manager: this });
48 | }
49 |
50 | public async close() {
51 | // @TODO close / dispose of any connections or other resources
52 | await this.clientServers.close();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/manager/src/storage/index.ts:
--------------------------------------------------------------------------------
1 | export type StorageItem = Record;
2 |
3 | export type Storage = StorageTable;
4 |
5 | export interface StorageTable {
6 | /** Inserts a new row into the table */
7 | insert(row: T): Promise;
8 |
9 | /** Creates or updates a row by its primary key */
10 | upsert({ id }: { id: string }, row: T): Promise;
11 |
12 | /** Updates an existing row by its primary key */
13 | update({ id }: { id: string }, row: Partial): Promise;
14 |
15 | /** Deletes a row by its primary key */
16 | delete({ id }: { id: string }): Promise;
17 |
18 | /** Retrieves rows matching a predicate */
19 | findMany(where?: Partial): Promise;
20 |
21 | /** Retrieves a row by its primary key */
22 | getById({ id }: { id: string }): Promise;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/manager/src/types.ts:
--------------------------------------------------------------------------------
1 | export type McpManagerId = string;
2 |
3 | export type ClientId = string;
4 |
5 | export type ServerId = string;
6 |
7 | export type ClientServerId = `${ClientId}-${ServerId}`;
8 |
9 | export type ToolName = string;
10 |
--------------------------------------------------------------------------------
/packages/manager/tests/manager.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 |
3 | import { createMcpManager } from '../src/manager.ts';
4 |
5 | describe('createManager()', () => {
6 | it('should create a manager', async () => {
7 | const manager = createMcpManager();
8 |
9 | await manager.servers.create({
10 | id: 'test',
11 | name: 'Test Server',
12 | version: '1.0.0',
13 | transport: {
14 | type: 'inMemory',
15 | config: {},
16 | },
17 | });
18 |
19 | expect(manager).toBeDefined();
20 | expect((await manager.servers.findMany()).length).toBe(1);
21 | expect(await manager.servers.get({ id: 'test' })).toBeDefined();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/manager/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.isomorphic.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "rootDir": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/manager/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig([
4 | {
5 | entry: ['src/index.ts'],
6 | format: ['esm'],
7 | sourcemap: true,
8 | clean: true,
9 | platform: 'node',
10 | env: {
11 | PLATFORM: 'node',
12 | },
13 | outDir: 'dist/node',
14 | },
15 | {
16 | entry: ['src/index.ts'],
17 | format: ['esm'],
18 | sourcemap: true,
19 | clean: true,
20 | platform: 'browser',
21 | env: {
22 | PLATFORM: 'browser',
23 | },
24 | outDir: 'dist/browser',
25 | },
26 | ]);
27 |
--------------------------------------------------------------------------------
/packages/openapi/.turbo/turbo-build.log:
--------------------------------------------------------------------------------
1 |
2 | [34mCLI[39m Building entry: src/index.ts
3 | [34mCLI[39m Using tsconfig: tsconfig.json
4 | [34mCLI[39m tsup v8.4.0
5 | [34mCLI[39m Using tsup config: /Users/marc/dev/openmcp/packages/openapi/tsup.config.ts
6 | [34mCLI[39m Target: es2023
7 | [34mCLI[39m Cleaning output folder
8 | [34mESM[39m Build start
9 | [32mESM[39m [1mdist/index.js [22m[32m7.21 KB[39m
10 | [32mESM[39m [1mdist/index.js.map [22m[32m15.53 KB[39m
11 | [32mESM[39m ⚡️ Build success in 8ms
12 |
--------------------------------------------------------------------------------
/packages/openapi/.turbo/turbo-lint.log:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/openapi/.turbo/turbo-test.log:
--------------------------------------------------------------------------------
1 |
2 | [?25l
3 | [1m[7m[36m RUN [39m[27m[22m [36mv3.1.1 [39m[90m/Users/marc/dev/openmcp/packages/openapi[39m
4 |
5 | [?2026h
6 | [1m[33m ❯ [39m[22mtests/index.test.ts[2m [queued][22m
7 |
8 | [2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
9 | [2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
10 | [2m Start at [22m14:04:47
11 | [2m Duration [22m201ms
12 | [?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
13 | [1m[33m ❯ [39m[22mtests/index.test.ts[2m 0/3[22m
14 |
15 | [2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
16 | [2m Tests [22m[1m[32m0 passed[39m[22m[90m (3)[39m
17 | [2m Start at [22m14:04:47
18 | [2m Duration [22m703ms
19 | [?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K
20 | [1m[33m ❯ [39m[22mtests/index.test.ts[2m 1/3[22m
21 |
22 | [2m Test Files [22m[1m[32m0 passed[39m[22m[90m (1)[39m
23 | [2m Tests [22m[1m[32m1 passed[39m[22m[90m (3)[39m
24 | [2m Start at [22m14:04:47
25 | [2m Duration [22m804ms
26 | [?2026l[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K [32m✓[39m tests/index.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 103[2mms[22m[39m
27 | [32m✓[39m createMcpServer[2m > [22mshould create a MCP server for petstore.json[32m 24[2mms[22m[39m
28 | [32m✓[39m createMcpServer[2m > [22mshould create a MCP server for slack.json[32m 52[2mms[22m[39m
29 | [32m✓[39m createMcpServer[2m > [22mshould create a MCP server for weather-gov.json[32m 24[2mms[22m[39m
30 |
31 | [2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
32 | [2m Tests [22m [1m[32m3 passed[39m[22m[90m (3)[39m
33 | [2m Start at [22m 14:04:47
34 | [2m Duration [22m 879ms[2m (transform 66ms, setup 0ms, collect 444ms, tests 103ms, environment 0ms, prepare 74ms)[22m
35 |
36 | [?25h
37 |
--------------------------------------------------------------------------------
/packages/openapi/.turbo/turbo-typecheck.log:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/openapi/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openmcp/openapi
2 |
3 | ## 0.1.2
4 |
5 | ### Patch Changes
6 |
7 | - [`7ee5b16`](https://github.com/getdatanaut/openmcp/commit/7ee5b169d621211ed85dbb11625a8dd6b951178b) Thanks
8 | [@P0lip](https://github.com/P0lip)! - Added a separate exports entry for the request client
9 |
10 | ## 0.1.1
11 |
12 | ### Patch Changes
13 |
14 | - [`00af5e2`](https://github.com/getdatanaut/openmcp/commit/00af5e2dc9e639c3877172bef5637e147bcd1b67) Thanks
15 | [@P0lip](https://github.com/P0lip)! - Minor adjustment in Blob availability detection
16 |
17 | ## 0.1.0
18 |
19 | ### Minor Changes
20 |
21 | - [`fc24b8d`](https://github.com/getdatanaut/openmcp/commit/fc24b8d5d47c9e7fb9f6bbc0498824432c0b432b) Thanks
22 | [@P0lip](https://github.com/P0lip)! - Introduced improvement request client
23 |
24 | ## 0.0.3
25 |
26 | ### Patch Changes
27 |
28 | - [`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371) Thanks
29 | [@P0lip](https://github.com/P0lip)! - Initial release
30 |
31 | - Updated dependencies
32 | [[`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371)]:
33 | - @openmcp/server@0.0.3
34 |
--------------------------------------------------------------------------------
/packages/openapi/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import { readdirSync } from 'node:fs';
2 | import { resolve } from 'node:path';
3 |
4 | import pickBy from 'lodash-es/pickBy.js';
5 | import { http, HttpResponse } from 'msw';
6 | import { setupServer } from 'msw/node';
7 | import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
8 |
9 | import { openApiToMcpServerOptions } from '../src/index.ts';
10 |
11 | const fixtureDir = resolve(__dirname, '__fixtures__/openapi');
12 | const fixtures = readdirSync(fixtureDir)
13 | .filter(file => file.endsWith('.json'))
14 | .map(file => {
15 | return [file, require(`${fixtureDir}/${file}`)];
16 | });
17 |
18 | const serverUrl = 'http://createMcpServer-test.com';
19 | export const restHandlers = [
20 | http.all(`${serverUrl}/*`, () => {
21 | return HttpResponse.json({ success: true });
22 | }),
23 | ];
24 |
25 | const server = setupServer(...restHandlers);
26 |
27 | describe('createMcpServer', () => {
28 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
29 | afterAll(() => server.close());
30 | afterEach(() => server.resetHandlers());
31 |
32 | test.each(fixtures)('should create a MCP server for %s', async (filename, openapi) => {
33 | const {
34 | options: { tools },
35 | } = await openApiToMcpServerOptions({ openapi, serverUrl });
36 |
37 | await expect(
38 | Object.entries(tools).map(([name, tool]) => ({
39 | name,
40 | hasDescription: !!tool.description,
41 | hasInputSchema: !!tool.parameters,
42 | hasOutputSchema: !!tool['output'],
43 | hints: pickBy(tool['annotations']?.hints, v => v),
44 | })),
45 | ).toMatchFileSnapshot(`./__snapshots__/openapi/${filename}.snap`);
46 |
47 | await expect(Object.values(tools)[0]!.execute({})).resolves.toHaveProperty('success', true);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/openapi/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default baseConfig;
4 |
--------------------------------------------------------------------------------
/packages/openapi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openmcp/openapi",
3 | "version": "0.1.2",
4 | "type": "module",
5 | "license": "MIT",
6 | "sideEffects": false,
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/getdatanaut/openmcp",
10 | "directory": "packages/openapi"
11 | },
12 | "files": [
13 | "dist",
14 | "src",
15 | "README.md",
16 | "CHANGELOG.md",
17 | "LICENSE",
18 | "package.json"
19 | ],
20 | "exports": {
21 | ".": {
22 | "types": "./src/index.ts",
23 | "development": "./src/index.ts",
24 | "default": "./dist/index.js"
25 | },
26 | "./client": {
27 | "types": "./src/client.ts",
28 | "development": "./src/client.ts",
29 | "default": "./dist/client.js"
30 | }
31 | },
32 | "scripts": {
33 | "build": "tsup",
34 | "lint": "eslint .",
35 | "test": "vitest run",
36 | "test.watch": "vitest watch",
37 | "typecheck": "tsc"
38 | },
39 | "dependencies": {
40 | "@apidevtools/json-schema-ref-parser": "^11.9.3",
41 | "@modelcontextprotocol/sdk": "^1.11.0",
42 | "@openmcp/server": "workspace:*",
43 | "@stoplight/http-spec": "^7.1.0",
44 | "@stoplight/json": "^3.21.7",
45 | "ai": "^4.3.13",
46 | "lodash-es": "^4.17.21",
47 | "neverthrow": "^8.2.0",
48 | "url-template": "^3.1.1"
49 | },
50 | "devDependencies": {
51 | "@datanaut/eslint-config": "0.1.1",
52 | "@datanaut/tsconfig": "0.1.3",
53 | "eslint": "9.26.0",
54 | "fetch-mock": "^12.5.2",
55 | "json-schema": "0.4.0",
56 | "msw": "2.7.6",
57 | "tsup": "8.4.0",
58 | "typescript": "5.8.3",
59 | "vitest": "3.1.3"
60 | },
61 | "publishConfig": {
62 | "access": "public"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/openapi/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Client, collectOperationClientMeta, type OperationClientMeta } from './client.ts';
2 | export type { ClientConfig, ServerConfig } from './openapi-to-mcp.ts';
3 | export { getToolName, openApiToMcpServerOptions } from './openapi-to-mcp.ts';
4 |
--------------------------------------------------------------------------------
/packages/openapi/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.isomorphic.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "rootDir": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/openapi/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | format: 'esm',
5 | sourcemap: true,
6 | clean: true,
7 | entry: ['src/index.ts', 'src/client.ts'],
8 | });
9 |
--------------------------------------------------------------------------------
/packages/schemas/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openmcp/schemas
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371) Thanks
8 | [@P0lip](https://github.com/P0lip)! - Initial release
9 |
--------------------------------------------------------------------------------
/packages/schemas/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default baseConfig;
4 |
--------------------------------------------------------------------------------
/packages/schemas/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openmcp/schemas",
3 | "version": "0.0.3",
4 | "type": "module",
5 | "license": "MIT",
6 | "sideEffects": false,
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/getdatanaut/openmcp",
10 | "directory": "packages/schemas"
11 | },
12 | "files": [
13 | "dist",
14 | "src",
15 | "README.md",
16 | "CHANGELOG.md",
17 | "LICENSE",
18 | "package.json"
19 | ],
20 | "exports": {
21 | "./mcp": {
22 | "types": "./src/mcp.ts",
23 | "development": "./src/mcp.ts",
24 | "default": "./dist/mcp.js"
25 | }
26 | },
27 | "scripts": {
28 | "build": "tsup",
29 | "lint": "eslint .",
30 | "typecheck": "tsc"
31 | },
32 | "dependencies": {
33 | "is-valid-path": "0.1.1",
34 | "zod": "^3.24.3"
35 | },
36 | "devDependencies": {
37 | "@datanaut/eslint-config": "0.1.1",
38 | "@datanaut/tsconfig": "0.1.3",
39 | "@types/is-valid-path": "^0.1.2",
40 | "eslint": "9.26.0",
41 | "tsup": "8.4.0",
42 | "typescript": "5.8.3"
43 | },
44 | "publishConfig": {
45 | "access": "public"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/schemas/src/mcp.ts:
--------------------------------------------------------------------------------
1 | import isValidPath from 'is-valid-path';
2 | import { z } from 'zod';
3 |
4 | export const ToolName = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/, 'Tool name must match /^[a-zA-Z0-9_-]{1,64}$/');
5 |
6 | export const OpenAPITransportSchema = z.object({
7 | type: z.literal('openapi'),
8 | openapi: z.string().refine(
9 | value => {
10 | return URL.canParse(value) || isValidPath(value);
11 | },
12 | {
13 | message: 'openapi must be a valid URL or file path',
14 | },
15 | ),
16 | serverUrl: z.string().url().optional(),
17 | path: z.record(z.unknown()).optional(),
18 | query: z.record(z.unknown()).optional(),
19 | headers: z.record(z.unknown()).optional(),
20 | body: z.record(z.unknown()).optional(),
21 | });
22 |
23 | export type OpenAPITransport = z.infer;
24 |
25 | export const StreamableHTTPTransportSchema = z.object({
26 | type: z.literal('streamable-http'),
27 | url: z.string().url(),
28 | headers: z.record(z.string()).optional(),
29 | });
30 |
31 | export type StreamableHTTPTransport = z.infer;
32 |
33 | export const SSETransportSchema = z.object({
34 | type: z.literal('sse'),
35 | url: z.string().url(),
36 | headers: z.record(z.string()).optional(),
37 | });
38 |
39 | export type SSETransport = z.infer;
40 |
41 | export const StdIOTransportSchema = z.object({
42 | type: z.literal('stdio'),
43 | command: z.string().nonempty('Command must be provided'),
44 | args: z.array(z.string()),
45 | env: z.record(z.string()).optional(),
46 | cwd: z.string().optional(),
47 | });
48 |
49 | export type StdIOTransport = z.infer;
50 |
51 | export const TransportSchema = z.discriminatedUnion('type', [
52 | StreamableHTTPTransportSchema,
53 | SSETransportSchema,
54 | OpenAPITransportSchema,
55 | StdIOTransportSchema,
56 | ]);
57 |
58 | export type Transport = z.infer;
59 |
--------------------------------------------------------------------------------
/packages/schemas/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.isomorphic.json",
3 | "include": ["**/*.ts", "**/*.tsx"],
4 | "compilerOptions": {
5 | "rootDir": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/schemas/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | format: 'esm',
5 | sourcemap: true,
6 | clean: true,
7 | entry: ['src/mcp.ts'],
8 | });
9 |
--------------------------------------------------------------------------------
/packages/server/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openmcp/server
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371) Thanks
8 | [@P0lip](https://github.com/P0lip)! - Initial release
9 |
10 | - Updated dependencies
11 | [[`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371)]:
12 | - @openmcp/utils@0.0.3
13 |
--------------------------------------------------------------------------------
/packages/server/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default baseConfig;
4 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openmcp/server",
3 | "version": "0.0.3",
4 | "type": "module",
5 | "license": "MIT",
6 | "sideEffects": false,
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/getdatanaut/openmcp",
10 | "directory": "packages/server"
11 | },
12 | "files": [
13 | "dist",
14 | "src",
15 | "README.md",
16 | "CHANGELOG.md",
17 | "LICENSE",
18 | "package.json"
19 | ],
20 | "exports": {
21 | ".": {
22 | "types": "./src/index.ts",
23 | "development": "./src/index.ts",
24 | "default": "./dist/index.js"
25 | }
26 | },
27 | "scripts": {
28 | "build": "tsup",
29 | "lint": "eslint .",
30 | "typecheck": "tsc"
31 | },
32 | "dependencies": {
33 | "@ai-sdk/ui-utils": "^1.2.10",
34 | "@modelcontextprotocol/sdk": "^1.11.0",
35 | "@openmcp/utils": "workspace:*",
36 | "ai": "^4.3.13",
37 | "zod": "^3.24.3"
38 | },
39 | "devDependencies": {
40 | "@datanaut/eslint-config": "0.1.1",
41 | "@datanaut/tsconfig": "0.1.3",
42 | "eslint": "9.26.0",
43 | "json-schema": "0.4.0",
44 | "tsup": "8.4.0",
45 | "typescript": "5.8.3",
46 | "vitest": "3.1.3"
47 | },
48 | "publishConfig": {
49 | "access": "public"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/server/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resources.ts';
2 | export type { OpenMcpServerOptions } from './server.ts';
3 | export { OpenMcpServer } from './server.ts';
4 | export * from './tools.ts';
5 |
--------------------------------------------------------------------------------
/packages/server/src/tools.ts:
--------------------------------------------------------------------------------
1 | import type { Schema } from 'ai';
2 | import type { z } from 'zod';
3 |
4 | // aisdk does not export these, so copied over
5 | export type ToolParameters = z.ZodTypeAny | Schema;
6 | export type inferToolParameters =
7 | Params extends Schema ? Params['_type'] : Params extends z.ZodTypeAny ? z.infer : never;
8 |
9 | export type ToolOutput = z.ZodTypeAny | Schema;
10 |
11 | export type ToolName = string;
12 |
13 | export type ToolAnnotations = Record> = {
14 | title?: string;
15 | hints?: Hints;
16 | };
17 |
18 | export interface McpServerTool<
19 | Params extends ToolParameters = ToolParameters,
20 | ToolResult = any,
21 | OutputSchema extends ToolOutput = ToolOutput,
22 | Annotations extends ToolAnnotations = ToolAnnotations,
23 | > {
24 | parameters?: Params;
25 | description?: string;
26 | output?: OutputSchema;
27 | execute: (args: inferToolParameters) => Promise;
28 | annotations?: Annotations;
29 | }
30 |
31 | export function tool<
32 | Params extends ToolParameters,
33 | ToolResult,
34 | OutputSchema extends ToolOutput,
35 | Annotations extends ToolAnnotations,
36 | >(tool: McpServerTool) {
37 | return tool;
38 | }
39 |
--------------------------------------------------------------------------------
/packages/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.isomorphic.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "rootDir": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/server/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | format: 'esm',
5 | sourcemap: true,
6 | clean: true,
7 | entry: ['src/index.ts'],
8 | });
9 |
--------------------------------------------------------------------------------
/packages/utils/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @openmcp/utils
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [`c3ec1af`](https://github.com/getdatanaut/openmcp/commit/c3ec1afdf557b8552d62a3981ced2bb2a5bf6371) Thanks
8 | [@P0lip](https://github.com/P0lip)! - Initial release
9 |
--------------------------------------------------------------------------------
/packages/utils/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { baseConfig } from '@datanaut/eslint-config/base';
2 |
3 | export default baseConfig;
4 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openmcp/utils",
3 | "version": "0.0.3",
4 | "type": "module",
5 | "license": "MIT",
6 | "sideEffects": false,
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/getdatanaut/openmcp",
10 | "directory": "packages/utils"
11 | },
12 | "files": [
13 | "dist",
14 | "src",
15 | "README.md",
16 | "CHANGELOG.md",
17 | "LICENSE",
18 | "package.json"
19 | ],
20 | "exports": {
21 | ".": {
22 | "types": "./src/index.ts",
23 | "development": "./src/index.ts",
24 | "default": "./dist/index.js"
25 | },
26 | "./documents": {
27 | "types": "./src/documents/index.ts",
28 | "development": "./src/documents/index.ts",
29 | "default": "./dist/documents/index.js"
30 | }
31 | },
32 | "scripts": {
33 | "build": "tsup",
34 | "lint": "eslint .",
35 | "test": "vitest run",
36 | "test.watch": "vitest watch",
37 | "typecheck": "tsc"
38 | },
39 | "dependencies": {
40 | "@stoplight/json": "^3.21.7",
41 | "@stoplight/yaml": "^4.3.0",
42 | "ai": "^4.3.13",
43 | "dedent": "^1.6.0",
44 | "gpt-tokenizer": "^2.9.0",
45 | "jsonpath-rfc9535": "^1.3.0",
46 | "lodash-es": "^4.17.21",
47 | "neverthrow": "^8.2.0",
48 | "zod": "^3.24.3"
49 | },
50 | "devDependencies": {
51 | "@datanaut/eslint-config": "0.1.1",
52 | "@datanaut/tsconfig": "0.1.3",
53 | "eslint": "9.26.0",
54 | "json-schema": "0.4.0",
55 | "tsup": "8.4.0",
56 | "typescript": "5.8.3",
57 | "vitest": "3.1.3"
58 | },
59 | "publishConfig": {
60 | "access": "public"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/invalid-document.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",,,,,
3 | }
4 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/invalid-document.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: Test API
4 | version: 1.0.0
5 | description: A test API for loadDocument
6 | paths:
7 | /test:
8 | get:
9 | summary: Test endpoint
10 | responses:
11 | '200':
12 | description: OK
13 | - not properly indented
14 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/non-object-document.json:
--------------------------------------------------------------------------------
1 | "This is a string, not an object"
2 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/unsupported-document.txt:
--------------------------------------------------------------------------------
1 | This is a text file with an unsupported extension.
2 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/valid-document.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "title": "Test API",
5 | "version": "1.0.0",
6 | "description": "A test API for loadDocument"
7 | },
8 | "paths": {
9 | "/test": {
10 | "get": {
11 | "summary": "Test endpoint",
12 | "responses": {
13 | "200": {
14 | "description": "OK"
15 | }
16 | }
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/valid-document.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Test Document",
3 | "version": "1.0.0",
4 | "description": "A test document for document utils",
5 | // comment
6 | "properties": {
7 | "name": {
8 | "type": "string",
9 | },
10 | "age": {
11 | "type": "number",
12 | },
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/valid-document.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: Test API
4 | version: 1.0.0
5 | description: A test API for loadDocument
6 | paths:
7 | /test:
8 | get:
9 | summary: Test endpoint
10 | responses:
11 | '200':
12 | description: OK
13 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/__tests__/fixtures/valid-document.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: Test API
4 | version: 1.0.0
5 | description: A test API for loadDocument
6 | paths:
7 | /test:
8 | get:
9 | summary: Test endpoint
10 | responses:
11 | '200':
12 | description: OK
13 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/index.ts:
--------------------------------------------------------------------------------
1 | export { default as loadDocument, type LoadOptions } from './load.ts';
2 | export { default as parseDocument } from './parse.ts';
3 | export { type IO, default as readDocument } from './read.ts';
4 | export { default as serializeDocument } from './serialize.ts';
5 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/load.ts:
--------------------------------------------------------------------------------
1 | import parseDocument from './parse.ts';
2 | import readDocument, { type IO } from './read.ts';
3 |
4 | export type LoadOptions = {
5 | /**
6 | * @defaultValue {false}
7 | */
8 | parseJsonAsJsonc?: boolean;
9 | };
10 |
11 | /**
12 | * Loads a document from a specified location, processes its content based on the
13 | * document type, and returns the parsed result.
14 | *
15 | * @param {IO} io - The IO interface providing methods for reading the document content.
16 | * @param location - The location or path from which the document will be loaded.
17 | * @param {LoadOptions} [options={}] - Optional configuration for loading the document, such as parsing options.
18 | * @return {Promise>} A promise that resolves to the parsed contents of the document as a record.
19 | */
20 | export default async function loadDocument(
21 | io: IO,
22 | location: string,
23 | options: LoadOptions = {},
24 | ): Promise> {
25 | const { content, type } = await readDocument(io, location);
26 | if (type === 'json' && options.parseJsonAsJsonc) {
27 | return parseDocument(content, 'jsonc');
28 | }
29 |
30 | return parseDocument(content, type);
31 | }
32 |
--------------------------------------------------------------------------------
/packages/utils/src/documents/serialize.ts:
--------------------------------------------------------------------------------
1 | import { safeStringify } from '@stoplight/yaml';
2 |
3 | /**
4 | * Serializes the provided document into the specified format.
5 | *
6 | * @param document - The document to be serialized.
7 | * @param type - The format type for serialization. Supported values are 'json', 'jsonc', 'yaml', and 'yml'.
8 | * @return The serialized string based on the specified format.
9 | * @throws {Error} Throws an error if the provided type is unsupported.
10 | */
11 | export default function serializeDocument(document: unknown, type: string): string {
12 | switch (type) {
13 | case 'jsonc':
14 | case 'json': {
15 | return JSON.stringify(document, null, 2);
16 | }
17 | case 'yaml':
18 | case 'yml': {
19 | return safeStringify(document, {
20 | noRefs: true,
21 | indent: 2,
22 | skipInvalid: true,
23 | });
24 | }
25 | default:
26 | throw new Error(`Unsupported file type: ${type}`);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { AutoTrimToolResultError } from './auto-trim.ts';
2 | export { addToolRequirementsToSchema, autoTrimToolResult } from './auto-trim.ts';
3 | export * as errors from './errors.ts';
4 | export { replaceVariables } from './replace-variables.ts';
5 |
--------------------------------------------------------------------------------
/packages/utils/src/replace-variables.ts:
--------------------------------------------------------------------------------
1 | const VAR_REGEX = /\{\{([^}]+)}}/g;
2 |
3 | export function replaceVariables(input: string, variables: Record): string {
4 | return input.replace(VAR_REGEX, (match, name) => String(variables[name] ?? match));
5 | }
6 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@datanaut/tsconfig/tsconfig.isomorphic.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "rootDir": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/utils/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | format: 'esm',
5 | sourcemap: true,
6 | clean: true,
7 | entry: ['src/index.ts', 'src/documents/index.ts'],
8 | });
9 |
--------------------------------------------------------------------------------
/scripts/openmcp-dev.sh:
--------------------------------------------------------------------------------
1 | NODE_ENV=development \
2 | NODE_OPTIONS="--conditions=development --loader=ts-node/esm" \
3 | TS_NODE_PROJECT="$(pwd)/packages/cli/tsconfig.json" \
4 | TS_NODE_TRANSPILE_ONLY=1 \
5 | openmcp "$@"
6 |
--------------------------------------------------------------------------------
/yarn.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('@yarnpkg/types')} */
2 | const { defineConfig } = require(`@yarnpkg/types`);
3 |
4 | // Example here: https://github.com/yarnpkg/berry/blob/master/yarn.config.cjs
5 |
6 | /**
7 | * This rule will enforce that a workspace MUST depend on the same version of
8 | * a dependency as the one used by the other workspaces.
9 | *
10 | * @param {Context} context
11 | */
12 | function enforceConsistentDependenciesAcrossTheProject({ Yarn }) {
13 | for (const dependency of Yarn.dependencies()) {
14 | if (dependency.type === `peerDependencies`) continue;
15 |
16 | // exception for react
17 | if (dependency.ident === 'react') continue;
18 |
19 | for (const otherDependency of Yarn.dependencies({ ident: dependency.ident })) {
20 | if (otherDependency.type === `peerDependencies`) continue;
21 |
22 | dependency.update(otherDependency.range);
23 | }
24 | }
25 | }
26 |
27 | module.exports = defineConfig({
28 | constraints: async ctx => {
29 | enforceConsistentDependenciesAcrossTheProject(ctx);
30 | },
31 | });
32 |
--------------------------------------------------------------------------------