Merge branch 'main' into chore/sdk-react-apollo-and-migration-guide
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,6 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **Note:** Bug reports that are clearly AI-generated will not be accepted and will be closed immediately. Please write your bug report in your own words.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,6 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **Note:** Feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your feature request in your own words.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -8,6 +8,8 @@
|
||||
|
||||
--- Delete everything below this line before submitting your PR ---
|
||||
|
||||
> **Note on AI-assisted contributions:** Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. **The PR description must be written in your own words and be clear and concise**. Please ensure you remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
|
||||
|
||||
### PR title format
|
||||
|
||||
The PR title must follow the following pattern:
|
||||
@@ -22,6 +24,7 @@ Where `TYPE` is:
|
||||
|
||||
Where `PKG` is:
|
||||
|
||||
- `auth`: For changes to the Nhost Auth service
|
||||
- `ci`: For general changes to the build and/or CI/CD pipeline
|
||||
- `cli`: For changes to the Nhost CLI
|
||||
- `codegen`: For changes to the code generator
|
||||
@@ -29,10 +32,11 @@ Where `PKG` is:
|
||||
- `deps`: For changes to dependencies
|
||||
- `docs`: For changes to the documentation
|
||||
- `examples`: For changes to the examples
|
||||
- `internal/lib`: For changes to Nhost's common libraries (internal)
|
||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||
- `nixops`: For changes to the NixOps
|
||||
- `storage`: For changes to the Nhost Storage
|
||||
- `storage`: For changes to the Nhost Storage service
|
||||
|
||||
Where `SUMMARY` is a short description of what the PR does.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ runs:
|
||||
|
||||
# Define valid types and packages
|
||||
VALID_TYPES="feat|fix|chore"
|
||||
VALID_PKGS="ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
|
||||
|
||||
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
||||
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
||||
@@ -31,11 +31,11 @@ runs:
|
||||
echo " - chore: mark this pull request as a maintenance item"
|
||||
echo ""
|
||||
echo "Valid PKGs:"
|
||||
echo " - ci, cli, codegen, dashboard, deps, docs, examples,"
|
||||
echo " - auth, ci, cli, codegen, dashboard, deps, docs, examples,"
|
||||
echo " - mintlify-openapi, nhost-js, nixops, storage"
|
||||
echo ""
|
||||
echo "Example: feat(cli): add new command for database migrations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ PR title is valid!"
|
||||
echo "✅ PR title is valid!"
|
||||
|
||||
84
.github/workflows/auth_checks.yaml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: "auth: check and build"
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/auth_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/wf_build_artifacts.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# auth
|
||||
- 'services/auth/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('push-{0}', github.sha) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
NHOST_PAT: ${{ secrets.NHOST_PAT }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
60
.github/workflows/auth_wf_release.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: "auth: release"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
GIT_REF:
|
||||
required: true
|
||||
type: string
|
||||
VERSION:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
push-docker-hub:
|
||||
uses: ./.github/workflows/wf_docker_push_image.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
push-docker-ecr:
|
||||
uses: ./.github/workflows/wf_docker_push_image_ecr.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
CONTAINER_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-central-1.amazonaws.com
|
||||
14
.github/workflows/ci_release.yaml
vendored
@@ -33,6 +33,20 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Extracted project: $PROJECT, version: $VERSION"
|
||||
|
||||
auth:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == 'auth'
|
||||
uses: ./.github/workflows/auth_wf_release.yaml
|
||||
with:
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: ${{ needs.extract-project.outputs.version }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
cli:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == 'cli'
|
||||
|
||||
4
.github/workflows/ci_update_changelog.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
project: [cli, dashboard, packages/nhost-js, services/storage]
|
||||
project: [cli, dashboard, packages/nhost-js, services/auth, services/storage]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
cd ${{ matrix.project }}
|
||||
TAG_NAME=$(make release-tag-name)
|
||||
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
||||
if git tag | grep -q "$TAG_NAME@$VERSION"; then
|
||||
if git tag | grep -qx "$TAG_NAME@$VERSION"; then
|
||||
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
|
||||
else
|
||||
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"
|
||||
|
||||
9
.github/workflows/cli_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "cli: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/cli_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
@@ -81,7 +80,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
7
.github/workflows/codegen_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "codegen: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/codegen_checks.yaml'
|
||||
@@ -48,7 +47,7 @@ jobs:
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
|
||||
14
.github/workflows/dashboard_checks.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "dashboard: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_build_artifacts.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
ENVIRONMENT: preview
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ needs.deploy-vercel.outputs.preview-url }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
|
||||
@@ -126,8 +126,10 @@ jobs:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY: ${{ secrets.PLAYWRIGHT_REPORT_ENCRYPTION_KEY }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
.github/workflows/dashboard_wf_e2e_staging.yaml
vendored
@@ -52,12 +52,16 @@ on:
|
||||
required: true
|
||||
NHOST_TEST_USER_PASSWORD:
|
||||
required: true
|
||||
NHOST_TEST_ONBOARDING_USER:
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET:
|
||||
required: true
|
||||
NHOST_TEST_FREE_USER_EMAILS:
|
||||
required: true
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_SUBDOMAIN:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_REGION:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: dashboard-e2e-staging
|
||||
@@ -77,7 +81,10 @@ env:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -141,7 +148,7 @@ jobs:
|
||||
rm playwright-report.tar.gz
|
||||
|
||||
- name: Upload encrypted Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
if: failure()
|
||||
with:
|
||||
name: encrypted-playwright-report-${{ github.run_id }}
|
||||
|
||||
1
.github/workflows/dashboard_wf_release.yaml
vendored
@@ -88,6 +88,7 @@ jobs:
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
find cli -type f -exec sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' {} +
|
||||
sed -i 's/nhost\/dashboard:[^)]*/nhost\/dashboard:${{ inputs.VERSION }}/g' docs/reference/cli/commands.mdx
|
||||
|
||||
- name: "Create Pull Request"
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
|
||||
8
.github/workflows/docs_checks.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "docs: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/dashboard_checks.yaml'
|
||||
@@ -28,6 +28,10 @@ on:
|
||||
# nhost-js
|
||||
- packages/nhost-js/**
|
||||
|
||||
# apis
|
||||
- 'services/auth/docs/openapi.yaml'
|
||||
- 'services/storage/controller/openapi.yaml'
|
||||
|
||||
# cli
|
||||
- cli/**
|
||||
push:
|
||||
@@ -58,7 +62,7 @@ jobs:
|
||||
with:
|
||||
NAME: docs
|
||||
PATH: docs
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
|
||||
7
.github/workflows/examples_demos_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/demos: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_demos_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/guides: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_guides_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/tutorials: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_tutorials_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
6
.github/workflows/gen_ai_review.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "gen: AI review"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
@@ -16,11 +16,13 @@ jobs:
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: Codium-ai/pr-agent@v0.30
|
||||
uses: Codium-ai/pr-agent@v0.31
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
config.model: ${{ vars.GEN_AI_MODEL }}
|
||||
config.model_turbo: $${{ vars.GEN_AI_MODEL_TURBO }}
|
||||
config.max_model_tokens: 200000
|
||||
config.custom_model_max_tokens: 200000
|
||||
ignore.glob: "['pnpm-lock.yaml','**/pnpm-lock.yaml', 'vendor/**','**/client_gen.go','**/models_gen.go','**/generated.go','**/*.gen.go']"
|
||||
|
||||
6
.github/workflows/gen_codeql-analysis.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
11
.github/workflows/nhost-js_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "nhost-js: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nhost-js_checks.yaml'
|
||||
@@ -34,6 +33,10 @@ on:
|
||||
|
||||
# nhost-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# apis
|
||||
- 'services/auth/docs/openapi.yaml'
|
||||
- 'services/storage/controller/openapi.yaml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -61,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -75,7 +78,7 @@ jobs:
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
|
||||
9
.github/workflows/nixops_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "nixops: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nixops_checks.yaml'
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -54,9 +53,9 @@ jobs:
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
|
||||
35
.github/workflows/nixops_wf_release.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "nixops: release"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'flake.lock'
|
||||
- 'nixops/project.nix'
|
||||
|
||||
jobs:
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
VERSION: latest
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
push-docker:
|
||||
uses: ./.github/workflows/wf_docker_push_image.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
VERSION: latest
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
8
.github/workflows/storage_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "storage: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/storage_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -18,6 +17,7 @@ on:
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# storage
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
NAME: storage
|
||||
PATH: services/storage
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: storage
|
||||
PATH: services/storage
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
|
||||
4
.github/workflows/wf_build_artifacts.yaml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
zip -r result.zip result
|
||||
|
||||
- name: "Push artifact to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-artifact-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result.zip
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
if: ${{ ( inputs.DOCKER ) }}
|
||||
|
||||
- name: "Push docker image to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-docker-image-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result
|
||||
|
||||
2
.github/workflows/wf_docker_push_image.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ linters:
|
||||
settings:
|
||||
funlen:
|
||||
lines: 65
|
||||
wsl_v5:
|
||||
allow-whole-block: true
|
||||
disable:
|
||||
- canonicalheader
|
||||
- depguard
|
||||
@@ -30,6 +32,7 @@ linters:
|
||||
- linters:
|
||||
- funlen
|
||||
- ireturn
|
||||
- goconst
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- lll
|
||||
|
||||
@@ -16,6 +16,15 @@ Contributions are made to Nhost repos via Issues and Pull Requests (PRs). A few
|
||||
- We work hard to make sure issues are handled on time, but it could take a while to investigate the root cause depending on the impact. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking.
|
||||
- If you've never contributed before, see [the first-timer's guide](https://github.com/firstcontributions/first-contributions) for resources and tips on getting started.
|
||||
|
||||
### AI-Assisted Contributions
|
||||
|
||||
We have specific policies regarding AI-assisted contributions:
|
||||
|
||||
- **Issues**: Bug reports and feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your issues in your own words to ensure they are clear, specific, and contain the necessary context.
|
||||
- **Pull Requests**: Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. The PR description must be written in your own words. Additionally, please remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
|
||||
|
||||
In all cases, contributors must ensure their submissions are thoughtful, well-tested, and meet the project's quality standards.
|
||||
|
||||
### Issues
|
||||
|
||||
Issues should be used to report problems with Nhost, request a new feature, or discuss potential changes before a PR is created.
|
||||
@@ -24,28 +33,20 @@ If you find an Issue that addresses the problem you're having, please add your r
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Please have a look at our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) to start coding!
|
||||
|
||||
PRs to our libraries are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should:
|
||||
|
||||
- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both.
|
||||
- Add unit or integration tests for fixed or changed functionality (if a test suite exists).
|
||||
- Address a single concern in the least number of changed lines as possible.
|
||||
- Include documentation in the repo or on our [docs site](https://docs.nhost.io).
|
||||
- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created).
|
||||
## Monorepo Structure
|
||||
|
||||
For changes that address core functionality or require breaking changes (e.g., a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes.
|
||||
This repository is a monorepo that contains multiple packages and applications. The structure is as follows:
|
||||
|
||||
In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
|
||||
- `cli` - The Nhost CLI
|
||||
- `dashboard` - The Nhost Dashboard
|
||||
- `docs` - Documentation
|
||||
- `examples` - Various example projects
|
||||
- `packages/nhost-js` - The Nhost JavaScript/TypeScript SDK
|
||||
- `services/auth` - Nhost Authentication service
|
||||
- `services/storage` - Nhost Storage service
|
||||
- `tools/codegen` - Internal code generation tool to build the SDK
|
||||
- `tools/mintlify-openapi` - Internal tool to generate reference documentation for Mintlify from an OpenAPI spec.
|
||||
|
||||
1. Fork the repository to your own Github account
|
||||
2. Clone the project to your machine
|
||||
3. Create a branch locally with a succinct but descriptive name. All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of:
|
||||
- `bug/` for bug fixes
|
||||
- `feat/` for features
|
||||
- `chore/` for configuration changes
|
||||
- `docs/` for documentation changes
|
||||
4. Commit changes to the branch
|
||||
5. Following any formatting and testing guidelines specific to this repo
|
||||
6. Push changes to your fork
|
||||
7. Open a PR in our repository and follow the PR template to review the changes efficiently.
|
||||
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
|
||||
100
DEVELOPERS.md
@@ -1,100 +0,0 @@
|
||||
# Developer Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
### Node.js v20 or later
|
||||
|
||||
### [pnpm](https://pnpm.io/) package manager
|
||||
|
||||
The easiest way to install `pnpm` if it's not installed on your machine yet is to use `npm`:
|
||||
|
||||
```sh
|
||||
$ npm install -g pnpm
|
||||
```
|
||||
|
||||
### [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
- The CLI is primarily used for running the E2E tests
|
||||
- Please refer to the [installation guide](https://docs.nhost.io/platform/cli/local-development) if you have not installed it yet
|
||||
|
||||
## File Structure
|
||||
|
||||
The repository is organized as a monorepo, with the following structure (only relevant folders are shown):
|
||||
|
||||
```
|
||||
assets/ # Assets used in the README
|
||||
config/ # Configuration files for the monorepo
|
||||
dashboard/ # Dashboard
|
||||
docs/ # Documentation website
|
||||
examples/ # Example projects
|
||||
packages/ # Core packages
|
||||
integrations/ # These are packages that rely on the core packages
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
### Installation
|
||||
|
||||
First, clone this repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/nhost
|
||||
```
|
||||
|
||||
Then, install the dependencies with `pnpm`:
|
||||
|
||||
```sh
|
||||
$ cd nhost
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Although package references are correctly updated on the fly for TypeScript, example projects and the dashboard won't see the changes because they are depending on the build output. To fix this, you can run packages in development mode.
|
||||
|
||||
Running packages in development mode from the root folder is as simple as:
|
||||
|
||||
```sh
|
||||
$ pnpm dev
|
||||
```
|
||||
|
||||
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Next.js and Vite automatically detect changes in the dependencies and rebuild everything, so the changes will be reflected in the examples and the dashboard.
|
||||
|
||||
**Note:** It's possible that Next.js or Vite throw an error when you run `pnpm dev`. Restarting the process should fix it.
|
||||
|
||||
### Use Examples
|
||||
|
||||
Examples are a great way to test your changes in practice. Make sure you've `pnpm dev` running in your terminal and then run an example.
|
||||
|
||||
Let's follow the instructions to run [react-apollo example](https://github.com/nhost/nhost/blob/main/examples/react-apollo/README.md).
|
||||
|
||||
## Edit Documentation
|
||||
|
||||
The easier way to contribute to our documentation is to go to the `docs` folder and follow the [instructions to start local development](https://github.com/nhost/nhost/blob/main/docs/README.md):
|
||||
|
||||
```sh
|
||||
$ cd docs
|
||||
# not necessary if you've already done this step somewhere in the repository
|
||||
$ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
## Run Test Suites
|
||||
|
||||
### Unit Tests
|
||||
|
||||
You can run the unit tests with the following command from the repository root:
|
||||
|
||||
```sh
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
Each package that defines end-to-end tests embeds their own Nhost configuration, that will be automatically when running the tests. As a result, you must make sure you are not running the Nhost CLI before running the tests.
|
||||
|
||||
You can run the e2e tests with the following command from the repository root:
|
||||
|
||||
```sh
|
||||
$ pnpm e2e
|
||||
```
|
||||
16
Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: envrc-install
|
||||
envrc-install: ## Copy envrc.sample to all project folders
|
||||
@for f in $$(find . -name "project.nix"); do \
|
||||
echo "Copying envrc.sample to $$(dirname $$f)/.envrc"; \
|
||||
cp ./envrc.sample $$(dirname $$f)/.envrc; \
|
||||
done
|
||||
|
||||
.PHONY: nixops-container-env
|
||||
nixops-container-env: ## Enter a NixOS container environment
|
||||
docker run \
|
||||
-it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ./:/build \
|
||||
-w /build \
|
||||
nixops:0.0.0-dev \
|
||||
bash
|
||||
@@ -12,7 +12,7 @@
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/blog">Blog</a>
|
||||
<span> • </span>
|
||||
<a href="https://twitter.com/nhost">Twitter</a>
|
||||
<a href="https://x.com/nhost">X</a>
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/discord">Discord</a>
|
||||
<span> • </span>
|
||||
@@ -33,10 +33,10 @@ Nhost consists of open source software:
|
||||
|
||||
- Database: [PostgreSQL](https://www.postgresql.org/)
|
||||
- Instant GraphQL API: [Hasura](https://hasura.io/)
|
||||
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/)
|
||||
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
|
||||
- Authentication: [Auth](https://github.com/nhost/nhost/tree/main/services/auth)
|
||||
- Storage: [Storage](https://github.com/nhost/nhost/tree/main/services/storage)
|
||||
- Serverless Functions: Node.js (JavaScript and TypeScript)
|
||||
- [Nhost CLI](https://docs.nhost.io/platform/cli/local-development) for local development
|
||||
- [Nhost CLI](https://github.com/nhost/nhost/tree/main/cli) for local development
|
||||
|
||||
## Architecture of Nhost
|
||||
|
||||
@@ -107,7 +107,6 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
# Resources
|
||||
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
// $schema provides code completion hints to IDEs.
|
||||
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
|
||||
"moderate": true,
|
||||
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
|
||||
"allowlist": [
|
||||
"GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
|
||||
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
|
||||
]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ get-version: ## Return version
|
||||
@echo $(VERSION)
|
||||
|
||||
|
||||
.PHONY: develop
|
||||
develop: ## Start a nix develop shell
|
||||
nix develop .\#$(NAME)
|
||||
|
||||
|
||||
.PHONY: _check-pre
|
||||
_check-pre: ## Pre-checks before running nix flake check
|
||||
|
||||
@@ -105,6 +110,11 @@ build-docker-image: ## Build docker container for native architecture
|
||||
skopeo copy --insecure-policy dir:./result docker-daemon:$(NAME):$(VERSION)
|
||||
|
||||
|
||||
.PHONY: build-docker-image-import-bare
|
||||
build-docker-image-import-bare:
|
||||
skopeo copy --insecure-policy dir:./result docker-daemon:$(NAME):$(VERSION)
|
||||
|
||||
|
||||
.PHONY: dev-env-up
|
||||
dev-env-up: _dev-env-build _dev-env-up ## Starts development environment
|
||||
|
||||
|
||||
@@ -1,7 +1,85 @@
|
||||
## [cli@1.34.7] - 2025-11-13
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Bump nhost/dashboard to 2.42.0 (#3693)
|
||||
|
||||
## [cli@1.34.6] - 2025-11-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Mcp: specify items type for arrays in tools (#3687)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Update bindings (#3689)
|
||||
|
||||
## [cli@1.34.5] - 2025-11-06
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
|
||||
- *(cli)* Udpate certs and schema (#3675)
|
||||
- *(cli)* Bump nhost/dashboard to 2.41.0 (#3669)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [cli@1.34.4] - 2025-10-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Update NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL correctly (#3643)
|
||||
|
||||
## [cli@1.34.3] - 2025-10-27
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Update schema (#3622)
|
||||
- *(cli)* Bump nhost/dashboard to 2.40.0 (#3629)
|
||||
|
||||
## [cli@1.34.2] - 2025-10-20
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Minor fix to download script when specifying version (#3602)
|
||||
- *(cli)* Update schema (#3613)
|
||||
|
||||
## [cli@1.34.1] - 2025-10-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Remove references to mcp-nhost (#3575)
|
||||
- *(cli)* Workaround os.Rename issues when src and dst are on different partitions (#3599)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(auth)* Change some references to deprecated hasura-auth (#3584)
|
||||
- *(docs)* Udpated README.md and CONTRIBUTING.md (#3587)
|
||||
|
||||
## [cli@1.34.0] - 2025-10-09
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(cli)* Added mcp server functionality from mcp-nhost (#3550)
|
||||
- *(cli)* Mcp: move configuration to .nhost folder and integrate cloud credentials (#3555)
|
||||
- *(cli)* Mcp: added support for environment variables in the configuration (#3556)
|
||||
- *(cli)* MCP refactor and documentation prior to official release (#3571)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(dashboard)* Remove NODE_ENV from restricted env vars (#3573)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(nixops)* Update nhost-cli (#3554)
|
||||
- *(cli)* Bump nhost/dashboard to 2.38.4 (#3539)
|
||||
|
||||
## [cli@1.33.0] - 2025-10-02
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
84
cli/CONTRIBUTING.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Developer Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
We use nix to manage the development environment, the build process and for running tests.
|
||||
|
||||
### With Nix (Recommended)
|
||||
|
||||
Run `nix develop \#cli` to get a complete development environment.
|
||||
|
||||
### Without Nix
|
||||
|
||||
Check `project.nix` (checkDeps, buildInputs, buildNativeInputs) for manual dependency installation. Alternatively, you can run `make nixops-container-env` in the root of the repository to enter a Docker container with nix and all dependencies pre-installed (note it is a large image).
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running Tests
|
||||
|
||||
**With Nix:**
|
||||
```bash
|
||||
make dev-env-up
|
||||
make check
|
||||
```
|
||||
|
||||
**Without Nix:**
|
||||
```bash
|
||||
# Start development environment
|
||||
make dev-env-up
|
||||
|
||||
# Lint Go code
|
||||
golangci-lint run ./...
|
||||
|
||||
# Run tests
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
Format code before committing:
|
||||
```bash
|
||||
golines -w --base-formatter=gofumpt .
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Local Build
|
||||
|
||||
Build the project (output in `./result`):
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Docker Image
|
||||
|
||||
Build and import Docker image with skopeo:
|
||||
```bash
|
||||
make build-docker-image
|
||||
```
|
||||
|
||||
If you run the command above inside the dockerized nixops-container-env and you get an error like:
|
||||
|
||||
```
|
||||
FATA[0000] writing blob: io: read/write on closed pipe
|
||||
```
|
||||
|
||||
then you need to run the following command outside of the container (needs skopeo installed on the host):
|
||||
|
||||
```bash
|
||||
cd cli
|
||||
make build-docker-image-import-bare
|
||||
```
|
||||
|
||||
### Multi-Platform Builds
|
||||
|
||||
Build for multiple platforms (Darwin/Linux, ARM64/AMD64):
|
||||
```bash
|
||||
make build-multiplatform
|
||||
```
|
||||
|
||||
This produces binaries for:
|
||||
- darwin/arm64
|
||||
- darwin/amd64
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
199
cli/MCP.md
@@ -1,199 +0,0 @@
|
||||
# nhost mcp
|
||||
|
||||
A Model Context Protocol (MCP) server implementation for interacting with Nhost Cloud projects and services.
|
||||
|
||||
## Overview
|
||||
|
||||
MCP-Nhost is designed to provide a unified interface for managing Nhost projects through the Model Context Protocol. It enables seamless interaction with Nhost Cloud services, offering a robust set of tools for project management and configuration.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The following tools are currently exposed through the MCP interface:
|
||||
|
||||
1. **cloud-get-graphql-schema**
|
||||
- Provides the GraphQL schema for the Nhost Cloud platform
|
||||
- Gives access to queries and mutations available for cloud management
|
||||
|
||||
2. **cloud-graphql-query**
|
||||
- Executes GraphQL queries and mutations against the Nhost Cloud platform
|
||||
- Enables project and organization management
|
||||
- Allows querying and updating project configurations
|
||||
- Mutations require enabling them when starting the server
|
||||
|
||||
3. **local-get-graphql-schema**
|
||||
- Retrieves the GraphQL schema for local Nhost development projects
|
||||
- Provides access to project-specific queries and mutations
|
||||
- Helps understand available operations for local development helping generating code
|
||||
- Uses "user" role unless specified otherwise
|
||||
|
||||
4. **local-graphql-query**
|
||||
- Executes GraphQL queries against local Nhost development projects
|
||||
- Enables testing and development of project-specific operations
|
||||
- Supports both queries and mutations for local development
|
||||
- Uses "user" role unless specified otherwise
|
||||
|
||||
5. **local-config-server-get-schema**
|
||||
- Retrieves the GraphQL schema for the local config server
|
||||
- Helps understand available configuration options for local projects
|
||||
|
||||
6. **local-config-server-query**
|
||||
- Executes GraphQL queries against the local config server
|
||||
- Enables querying and modifying local project configuration
|
||||
- Changes require running 'nhost up' to take effect
|
||||
|
||||
7. **local-get-management-graphql-schema**
|
||||
- Retrieves the GraphQL management schema for local projects
|
||||
- Useful for understanding how to manage Hasura metadata, migrations, and permissions
|
||||
- Provides insight into available management operations before using the management tool
|
||||
|
||||
8. **local-manage-graphql**
|
||||
- Interacts with GraphQL's management endpoints for local projects
|
||||
- Manages Hasura metadata, migrations, permissions, and remote schemas
|
||||
- Creates and applies database migrations
|
||||
- Handles data and schema changes through proper migration workflows
|
||||
- Manages roles and permissions
|
||||
|
||||
9. **project-get-graphql-schema**
|
||||
- Retrieves the GraphQL schema for Nhost Cloud projects
|
||||
- Provides access to project-specific queries and mutations
|
||||
- Uses "user" role unless specified otherwise
|
||||
|
||||
10. **project-graphql-query**
|
||||
- Executes GraphQL queries against Nhost Cloud projects
|
||||
- Enables interaction with live project data
|
||||
- Supports both queries and mutations (need to be allowed)
|
||||
- Uses "user" role unless specified otherwise
|
||||
|
||||
11. **search**
|
||||
- Searches Nhost's official documentation
|
||||
- Provides information about Nhost features, APIs, and guides
|
||||
- Helps find relevant documentation for implementing features or solving issues
|
||||
- Returns links to detailed documentation pages
|
||||
|
||||
## Screenshots and Examples
|
||||
|
||||
You can find screenshots and examples of the current features and tools in the [screenshots](docs/mcp/screenshots.md) file.
|
||||
|
||||
## Installing
|
||||
|
||||
To install mcp-nhost, you can use the following command:
|
||||
|
||||
```bash
|
||||
sudo curl -L https://raw.githubusercontent.com/nhost/mcp-nhost/main/get.sh | bash
|
||||
```
|
||||
|
||||
## Configuring
|
||||
|
||||
After installing mcp-nhost, you will need to configure it. You can do this by running the command `mcp-nhost config` in your terminal. See [CONFIG.md](docs/mcp/CONFIG.md) for more details.
|
||||
|
||||
## Configuring clients
|
||||
|
||||
#### Cursor
|
||||
|
||||
1. Go to "Cursor Settings"
|
||||
2. Click on "MCP"
|
||||
3. Click on "+ Add new global MCP server"
|
||||
4. Add the following object inside `"mcpServers"`:
|
||||
|
||||
```json
|
||||
"mcp-nhost": {
|
||||
"command": "/usr/local/bin/mcp-nhost",
|
||||
"args": [
|
||||
"start",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
For help on how to use the CLI, you can run:
|
||||
|
||||
```bash
|
||||
mcp-nhost --help
|
||||
```
|
||||
|
||||
Or check [USAGE.md](docs/mcp/USAGE.md) for more details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you run into issues using the MCP server you can try running the tools yourself. For example:
|
||||
|
||||
```
|
||||
# cloud-get-graphql-schema
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"cloud-get-graphql-schema","arguments":{}},"id":1}' | mcp-nhost start
|
||||
|
||||
# cloud-graphql-query
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"cloud-graphql-query","arguments":{"query":"{ apps { id subdomain name } }"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-get-graphql-schema
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-get-graphql-schema","arguments":{"role":"user"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-graphql-query
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-graphql-query","arguments":{"query":"{ users { id } }", "role":"admin"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-config-server-get-schema
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-config-server-get-schema","arguments":{}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-config-server-query
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-config-server-query","arguments":{"query":"{ config(appID: \"00000000-0000-0000-0000-000000000000\", resolve: true) { postgres { version } } }"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-get-management-graphql-schema
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-get-management-graphql-schema","arguments":{}},"id":1}' | mcp-nhost start
|
||||
|
||||
# local-manage-graphql
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"local-manage-graphql","arguments":{"body":"{\"type\":\"export_metadata\",\"args\":{}}","endpoint":"https://local.hasura.local.nhost.run/v1/metadata"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# project-get-graphql-schema - set projectSubdomain to your own project
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"project-get-graphql-schema","arguments":{"projectSubdomain":"replaceMe", "role": "user"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# project-graphql-query - set projectSubdomain to your own project
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"project-graphql-query","arguments":{"projectSubdomain":"replaceMe","query":"{ users { id } }", "role":"admin"}},"id":1}' | mcp-nhost start
|
||||
|
||||
# search
|
||||
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search","arguments":{"query":"how to enable magic links"}},"id":1}' | mcp-nhost start
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- ✅ Cloud platform: Basic project and organization management
|
||||
- ✅ Cloud projects: Configuration management
|
||||
- ✅ Local projects: Configuration management
|
||||
- ✅ Local projects: Graphql Schema awareness and query execution
|
||||
- ✅ Cloud projects: Schema awareness and query execution
|
||||
- ✅ Local projects: Create migrations
|
||||
- ✅ Local projects: Manage permissions and relationships
|
||||
- ✅ Documentation: integrate or document use of mintlify's mcp server
|
||||
- ✅ Local projects: Auth and Storage schema awareness (maybe via mintlify?)
|
||||
- ✅ Cloud projects: Auth and Storage schema awareness (maybe via mintlify?)
|
||||
- 🔄 Local projects: Manage more metadata
|
||||
|
||||
If you have any suggestions or feature requests, please feel free to open an issue for discussion.
|
||||
|
||||
## Security and Privacy
|
||||
|
||||
### Enhanced Protection Layer
|
||||
|
||||
The MCP server is designed with security at its core, providing an additional protection layer beyond your existing GraphQL permissions. Key security features include:
|
||||
|
||||
- **Authentication enforcement** for all requests
|
||||
- **Permission and role respect** based on your existing authorization system and the credentials provided
|
||||
- **Query/mutation filtering** to further restrict allowed operations
|
||||
|
||||
### Granular Access Control
|
||||
|
||||
One of the MCP server's key security advantages is the ability to specify exactly which operations can pass through, even for authenticated users:
|
||||
|
||||
```toml
|
||||
[[projects]]
|
||||
subdomain = "my-blog"
|
||||
region = "eu-central-1"
|
||||
pat = "nhp_project_specific_pat"
|
||||
allow_queries = ["getBlogs", "getCommends"]
|
||||
allow_mutations = ["insertBlog", "insertComment"]
|
||||
```
|
||||
|
||||
With the configuration above, an LLM will be able to only execute the queries and mutations above on behalf of a user even if the user has broader permissions in the Nhost project.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to mcp-nhost! If you have suggestions, bug reports, or feature requests, please open an issue or submit a pull request.
|
||||
@@ -51,11 +51,18 @@ nhost up
|
||||
nhost up --ui nhost
|
||||
```
|
||||
|
||||
## MCP Server
|
||||
|
||||
The Nhost cli ships with an MCP server that lets you interact with your Nhost projects through AI assistants using the Model Context Protocol. It provides secure, controlled access to your GraphQL data, project configuration, and documentation—with granular permissions that let you specify exactly which queries and mutations an LLM can execute. For development, it streamlines your workflow by enabling AI-assisted schema management, metadata changes, and migrations, while providing direct access to your GraphQL schema for intelligent query building.
|
||||
|
||||
You can read more about the MCP server in the [MCP Server documentation](https://docs.nhost.io/platform/cli/mcp/overview).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Get started with Nhost CLI (longer version)](https://docs.nhost.io/platform/overview/get-started-with-nhost-cli)
|
||||
- [Nhost CLI](https://docs.nhost.io/platform/cli)
|
||||
- [Reference](https://docs.nhost.io/reference/cli)
|
||||
- [MCP Server](https://docs.nhost.io/platform/cli/mcp/overview)
|
||||
|
||||
## Build from Source
|
||||
|
||||
|
||||
@@ -29,3 +29,12 @@ func (ce *CliEnv) LoadSession(
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (ce *CliEnv) Credentials() (credentials.Credentials, error) {
|
||||
var creds credentials.Credentials
|
||||
if err := UnmarshalFile(ce.Path.AuthFile(), &creds, json.Unmarshal); err != nil {
|
||||
return credentials.Credentials{}, err
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.38.0",
|
||||
Value: "nhost/dashboard:2.42.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -111,7 +111,7 @@ func CommandUp() *cli.Command { //nolint:funlen
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.38.0",
|
||||
Value: "nhost/dashboard:2.42.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -12,8 +12,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
flagConfigFile = "config-file"
|
||||
flagConfirm = "confirm"
|
||||
flagConfirm = "confirm"
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
@@ -21,12 +20,6 @@ func Command() *cli.Command {
|
||||
Name: "config",
|
||||
Usage: "Generate and save configuration file",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigFile,
|
||||
Usage: "Configuration file path",
|
||||
Value: config.GetConfigPath(),
|
||||
Sources: cli.EnvVars("CONFIG_FILE"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagConfirm,
|
||||
Usage: "Skip confirmation prompt",
|
||||
@@ -36,16 +29,9 @@ func Command() *cli.Command {
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "dump",
|
||||
Usage: "Dump the configuration to stdout for verification",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigFile,
|
||||
Usage: "Path to the config file",
|
||||
Value: config.GetConfigPath(),
|
||||
Sources: cli.EnvVars("CONFIG_FILE"),
|
||||
},
|
||||
},
|
||||
Name: "dump",
|
||||
Usage: "Dump the configuration to stdout for verification",
|
||||
Flags: []cli.Flag{},
|
||||
Action: actionDump,
|
||||
},
|
||||
},
|
||||
@@ -70,7 +56,7 @@ func action(_ context.Context, cmd *cli.Command) error {
|
||||
fmt.Println(string(tomlData))
|
||||
fmt.Println()
|
||||
|
||||
filePath := cmd.String(flagConfigFile)
|
||||
filePath := config.GetConfigPath(cmd)
|
||||
fmt.Printf("Save configuration to %s?\n", filePath)
|
||||
fmt.Print("Proceed? (y/N): ")
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
)
|
||||
|
||||
func actionDump(_ context.Context, cmd *cli.Command) error {
|
||||
configPath := cmd.String(flagConfigFile)
|
||||
configPath := config.GetConfigPath(cmd)
|
||||
if configPath == "" {
|
||||
return cli.Exit("config file path is required", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
fmt.Println("Please, run `nhost mcp config` to configure the service.") //nolint:forbidigo
|
||||
return cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,23 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagConfigFile = "config-file"
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "mcp",
|
||||
Aliases: []string{},
|
||||
Usage: "Model Context Protocol (MCP) related commands",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigFile,
|
||||
Usage: "Configuration file path. Defaults to $NHOST_DOT_NHOST_FOLDER/nhost-mcp.toml",
|
||||
Value: "",
|
||||
Sources: cli.EnvVars("NHOST_MCP_CONFIG_FILE"),
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
config.Command(),
|
||||
start.Command(),
|
||||
|
||||
@@ -13,26 +13,39 @@ import (
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
nhostmcp "github.com/nhost/nhost/cli/cmd/mcp"
|
||||
"github.com/nhost/nhost/cli/cmd/mcp/start"
|
||||
"github.com/nhost/nhost/cli/cmd/user"
|
||||
"github.com/nhost/nhost/cli/mcp/resources"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/cloud"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/docs"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/local"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/project"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/schemas"
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
t.Parallel()
|
||||
|
||||
cmd := nhostmcp.Command()
|
||||
func TestStart(t *testing.T) { //nolint:cyclop,maintidx,paralleltest
|
||||
loginCmd := user.CommandLogin()
|
||||
mcpCmd := nhostmcp.Command()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
cmd.Writer = buf
|
||||
mcpCmd.Writer = buf
|
||||
|
||||
go func() {
|
||||
if err := cmd.Run(
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
if err := loginCmd.Run(
|
||||
context.Background(),
|
||||
[]string{
|
||||
"main",
|
||||
"--pat=user-pat",
|
||||
},
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := mcpCmd.Run(
|
||||
context.Background(),
|
||||
[]string{
|
||||
"main",
|
||||
@@ -54,7 +67,7 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
|
||||
mcpClient := client.NewClient(transportClient)
|
||||
|
||||
if err := mcpClient.Start(context.Background()); err != nil {
|
||||
if err := mcpClient.Start(t.Context()); err != nil {
|
||||
t.Fatalf("failed to start mcp client: %v", err)
|
||||
}
|
||||
defer mcpClient.Close()
|
||||
@@ -84,8 +97,14 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
Experimental: nil,
|
||||
Logging: nil,
|
||||
Prompts: nil,
|
||||
Resources: nil,
|
||||
Sampling: nil,
|
||||
Resources: &struct {
|
||||
Subscribe bool "json:\"subscribe,omitempty\""
|
||||
ListChanged bool "json:\"listChanged,omitempty\""
|
||||
}{
|
||||
Subscribe: false,
|
||||
ListChanged: false,
|
||||
},
|
||||
Sampling: nil,
|
||||
Tools: &struct {
|
||||
ListChanged bool "json:\"listChanged,omitempty\""
|
||||
}{
|
||||
@@ -96,7 +115,23 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
Name: "mcp",
|
||||
Version: "",
|
||||
},
|
||||
Instructions: start.ServerInstructions,
|
||||
Instructions: start.ServerInstructions + `
|
||||
|
||||
Configured projects:
|
||||
- local (local): Local development project running via the Nhost CLI
|
||||
- asdasdasdasdasd (eu-central-1): Staging project for my awesome app
|
||||
- qweqweqweqweqwe (us-east-1): Production project for my awesome app
|
||||
|
||||
The following resources are available:
|
||||
|
||||
- schema://nhost-cloud: Schema to interact with the Nhost Cloud. Projects are equivalent
|
||||
to apps in the schema. IDs are typically uuids.
|
||||
- schema://graphql-management: GraphQL's management schema for an Nhost project.
|
||||
This tool is useful to properly understand how manage hasura metadata, migrations,
|
||||
permissions, remote schemas, etc.
|
||||
- schema://nhost.toml: Cuelang schema for the nhost.toml configuration file. Run nhost
|
||||
config validate after making changes to your nhost.toml file to ensure it is valid.
|
||||
`,
|
||||
Result: mcp.Result{
|
||||
Meta: nil,
|
||||
},
|
||||
@@ -118,22 +153,6 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
//nolint:exhaustruct,lll
|
||||
&mcp.ListToolsResult{
|
||||
Tools: []mcp.Tool{
|
||||
{
|
||||
Name: "cloud-get-graphql-schema",
|
||||
Description: cloud.ToolGetGraphqlSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: nil,
|
||||
Required: nil,
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Cloud Platform",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cloud-graphql-query",
|
||||
Description: cloud.ToolGraphqlQueryInstructions,
|
||||
@@ -160,24 +179,40 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-config-server-get-schema",
|
||||
Description: local.ToolConfigServerSchemaInstructions,
|
||||
Name: "get-schema",
|
||||
Description: schemas.ToolGetGraphqlSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"includeMutations": map[string]any{
|
||||
"description": "include mutations in the schema",
|
||||
"type": "boolean",
|
||||
"role": map[string]any{
|
||||
"description": string("role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ"),
|
||||
"type": string("string"),
|
||||
},
|
||||
"includeQueries": map[string]any{
|
||||
"description": "include queries in the schema",
|
||||
"type": "boolean",
|
||||
"subdomain": map[string]any{
|
||||
"description": string("Project to get the GraphQL schema for. Required when service is `project`"),
|
||||
"enum": []any{string("local"), string("asdasdasdasdasd"), string("qweqweqweqweqwe")},
|
||||
"type": string("string"),
|
||||
},
|
||||
"mutations": map[string]any{
|
||||
"description": string("list of mutations to fetch"),
|
||||
"type": string("array"),
|
||||
"items": map[string]any{"type": string("string")},
|
||||
},
|
||||
"queries": map[string]any{
|
||||
"description": string("list of queries to fetch"),
|
||||
"type": string("array"),
|
||||
"items": map[string]any{"type": string("string")},
|
||||
},
|
||||
"summary": map[string]any{
|
||||
"default": bool(true),
|
||||
"description": string("only return a summary of the schema"),
|
||||
"type": string("boolean"),
|
||||
},
|
||||
},
|
||||
Required: []string{"includeQueries", "includeMutations"},
|
||||
Required: []string{"role", "subdomain"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Config Server",
|
||||
Title: "Get GraphQL/API schema for various services",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
@@ -185,109 +220,7 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-config-server-query",
|
||||
Description: local.ToolConfigServerQueryInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"query": map[string]any{
|
||||
"description": "graphql query to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"variables": map[string]any{
|
||||
"description": "variables to use in the query",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Config Server",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-get-graphql-schema",
|
||||
Description: local.ToolGetGraphqlSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"role": map[string]any{
|
||||
"description": "role to use when executing queries. Default to user but make sure the user is aware",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"role"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Development Project",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-graphql-query",
|
||||
Description: local.ToolGraphqlQueryInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"query": map[string]any{
|
||||
"description": "graphql query to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"role": map[string]any{
|
||||
"description": "role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
|
||||
"type": "string",
|
||||
},
|
||||
"variables": map[string]any{
|
||||
"additionalProperties": true,
|
||||
"description": "variables to use in the query",
|
||||
"properties": map[string]any{},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
Required: []string{"query", "role"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Development Project",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "project-get-graphql-schema",
|
||||
Description: project.ToolGetGraphqlSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"projectSubdomain": map[string]any{
|
||||
"description": "Project to get the GraphQL schema for. Must be one of asdasdasdasdasd, qweqweqweqweqwe, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names",
|
||||
"type": "string",
|
||||
},
|
||||
"role": map[string]any{
|
||||
"description": "role to use when executing queries. Default to user but make sure the user is aware",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"role", "projectSubdomain"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Project running on Nhost Cloud",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "project-graphql-query",
|
||||
Name: "graphql-query",
|
||||
Description: project.ToolGraphqlQueryInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
@@ -296,12 +229,17 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
"description": "graphql query to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"projectSubdomain": map[string]any{
|
||||
"description": "Project to get the GraphQL schema for. Must be one of asdasdasdasdasd, qweqweqweqweqwe, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names",
|
||||
"subdomain": map[string]any{
|
||||
"description": "Project to perform the GraphQL query against",
|
||||
"type": "string",
|
||||
"enum": []any{
|
||||
string("local"),
|
||||
string("asdasdasdasdasd"),
|
||||
string("qweqweqweqweqwe"),
|
||||
},
|
||||
},
|
||||
"role": map[string]any{
|
||||
"description": "role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
|
||||
"description": "role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
|
||||
"type": "string",
|
||||
},
|
||||
"userId": map[string]any{
|
||||
@@ -313,7 +251,7 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"query", "projectSubdomain", "role"},
|
||||
Required: []string{"query", "subdomain", "role"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Project running on Nhost Cloud",
|
||||
@@ -324,36 +262,30 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-get-management-graphql-schema",
|
||||
Description: local.ToolGetGraphqlManagementSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: nil,
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL's Management Schema for Nhost Development Project",
|
||||
ReadOnlyHint: ptr(true),
|
||||
IdempotentHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "local-manage-graphql",
|
||||
Description: local.ToolManageGraphqlInstructions,
|
||||
Name: "manage-graphql",
|
||||
Description: project.ToolManageGraphqlInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"body": map[string]any{
|
||||
"description": string("The body for the HTTP request"),
|
||||
"type": string("string"),
|
||||
"description": "The body for the HTTP request",
|
||||
"type": "string",
|
||||
},
|
||||
"endpoint": map[string]any{
|
||||
"description": string("The GraphQL management endpoint to query. Use https://local.hasura.local.nhost.run as base URL"),
|
||||
"type": string("string"),
|
||||
"path": map[string]any{
|
||||
"description": "The path for the HTTP request",
|
||||
"type": "string",
|
||||
},
|
||||
"subdomain": map[string]any{
|
||||
"description": "Project to perform the GraphQL management operation against",
|
||||
"type": "string",
|
||||
"enum": []any{
|
||||
string("local"),
|
||||
string("asdasdasdasdasd"),
|
||||
string("qweqweqweqweqwe"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"endpoint", "body"},
|
||||
Required: []string{"subdomain", "path", "body"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Manage GraphQL's Metadata on an Nhost Development Project",
|
||||
@@ -393,24 +325,60 @@ func TestStart(t *testing.T) { //nolint:cyclop,maintidx
|
||||
t.Errorf("ListToolsResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if res.Capabilities.Resources != nil {
|
||||
resources, err := mcpClient.ListResources(
|
||||
context.Background(),
|
||||
mcp.ListResourcesRequest{}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list resources: %v", err)
|
||||
}
|
||||
resourceList, err := mcpClient.ListResources(
|
||||
context.Background(),
|
||||
mcp.ListResourcesRequest{}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list resources: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(
|
||||
resources,
|
||||
//nolint:exhaustruct
|
||||
&mcp.ListResourcesResult{
|
||||
Resources: []mcp.Resource{},
|
||||
if diff := cmp.Diff(
|
||||
resourceList,
|
||||
//nolint:exhaustruct
|
||||
&mcp.ListResourcesResult{
|
||||
Resources: []mcp.Resource{
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://graphql-management",
|
||||
Name: "graphql-management",
|
||||
Description: resources.GraphqlManagementDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://nhost-cloud",
|
||||
Name: "nhost-cloud",
|
||||
Description: resources.CloudDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://nhost.toml",
|
||||
Name: "nhost.toml",
|
||||
Description: resources.NhostTomlResourceDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
},
|
||||
); diff != "" {
|
||||
t.Errorf("ListResourcesResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
},
|
||||
); diff != "" {
|
||||
t.Errorf("ListResourcesResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if res.Capabilities.Prompts != nil {
|
||||
|
||||
@@ -5,17 +5,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
"github.com/nhost/nhost/cli/mcp/resources"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/cloud"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/docs"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/local"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/project"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/schemas"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagConfigFile = "config-file"
|
||||
flagNhostAuthURL = "nhost-auth-url"
|
||||
flagNhostGraphqlURL = "nhost-graphql-url"
|
||||
flagBind = "bind"
|
||||
@@ -25,22 +26,21 @@ const (
|
||||
// this seems to be largely ignored by clients, or at least by cursor.
|
||||
// we also need to look into roots and resources as those might be helpful.
|
||||
ServerInstructions = `
|
||||
This is an MCP server to interact with Nhost Cloud and with projects running on it and
|
||||
also with Nhost local development projects.
|
||||
This is an MCP server to interact with the Nhost Cloud and with Nhost projects.
|
||||
|
||||
Important notes to anyone using this MCP server. Do not use this MCP server without
|
||||
following these instructions:
|
||||
Important notes to anyone using this MCP server. Do not use this MCP server without
|
||||
following these instructions:
|
||||
|
||||
1. Make sure you are clear on which environment the user wants to operate against.
|
||||
2. Before attempting to call any tool *-graphql-query, always get the schema using the
|
||||
*-get-graphql-schema tool
|
||||
3. Apps and projects are the same and while users may talk about projects in the GraphQL
|
||||
api those are referred as apps.
|
||||
4. IDs are always UUIDs so if you have anything else (like an app/project name) you may need
|
||||
to first get the ID using the *-graphql-query tool.
|
||||
5. If you have an error querying the GraphQL API, please check the schema again. The schema may
|
||||
have changed and the query you are using may be invalid.
|
||||
`
|
||||
1. Make sure you are clear on which environment the user wants to operate against.
|
||||
2. Before attempting to call any tool, always make sure you list resources, roots, and
|
||||
resource templates to understand what is available.
|
||||
3. Apps and projects are the same and while users may talk about projects in Nhost's GraphQL
|
||||
api those are referred as apps.
|
||||
4. If you have an error querying the GraphQL API, please check the schema again. The schema may
|
||||
have changed and the query you are using may be invalid.
|
||||
5. Always follow the instructions provided by each tool. If you need to deviate from these
|
||||
instructions, please, confirm with the user before doing so.
|
||||
`
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
@@ -48,12 +48,6 @@ func Command() *cli.Command {
|
||||
Name: "start",
|
||||
Usage: "Starts the MCP server",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigFile,
|
||||
Usage: "Path to the config file",
|
||||
Value: config.GetConfigPath(),
|
||||
Sources: cli.EnvVars("CONFIG_FILE"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostAuthURL,
|
||||
Usage: "Nhost auth URL",
|
||||
@@ -88,14 +82,25 @@ func action(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ServerInstructions := ServerInstructions
|
||||
ServerInstructions += "\n\n"
|
||||
ServerInstructions += cfg.Projects.Instructions()
|
||||
ServerInstructions += "\n"
|
||||
ServerInstructions += resources.Instructions()
|
||||
|
||||
mcpServer := server.NewMCPServer(
|
||||
cmd.Root().Name,
|
||||
cmd.Root().Version,
|
||||
server.WithInstructions(ServerInstructions),
|
||||
)
|
||||
|
||||
if err := resources.Register(cfg, mcpServer); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register resources: %s", err), 1)
|
||||
}
|
||||
|
||||
if cfg.Cloud != nil {
|
||||
if err := registerCloud(
|
||||
cmd,
|
||||
mcpServer,
|
||||
cfg,
|
||||
cmd.String(flagNhostAuthURL),
|
||||
@@ -105,18 +110,15 @@ func action(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Local != nil {
|
||||
if err := registerLocal(mcpServer, cfg); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register local tools: %s", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Projects) > 0 {
|
||||
if err := registerProjectTool(mcpServer, cfg); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register project tools: %s", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
resources := schemas.NewTool(cfg)
|
||||
resources.Register(mcpServer)
|
||||
|
||||
d, err := docs.NewTool(ctx)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to initialize docs tools: %s", err), 1)
|
||||
@@ -128,14 +130,14 @@ func action(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
func getConfig(cmd *cli.Command) (*config.Config, error) {
|
||||
configPath := cmd.String(flagConfigFile)
|
||||
configPath := config.GetConfigPath(cmd)
|
||||
if configPath == "" {
|
||||
return nil, cli.Exit("config file path is required", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
fmt.Println("Please, run `nhost mcp config` to configure the service.") //nolint:forbidigo
|
||||
return nil, cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
@@ -143,14 +145,22 @@ func getConfig(cmd *cli.Command) (*config.Config, error) {
|
||||
}
|
||||
|
||||
func registerCloud(
|
||||
cmd *cli.Command,
|
||||
mcpServer *server.MCPServer,
|
||||
cfg *config.Config,
|
||||
authURL string,
|
||||
graphqlURL string,
|
||||
) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
creds, err := ce.Credentials()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load credentials: %w", err)
|
||||
}
|
||||
|
||||
interceptor, err := auth.WithPAT(
|
||||
authURL,
|
||||
cfg.Cloud.PAT,
|
||||
creds.PersonalAccessToken,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create PAT interceptor: %w", err)
|
||||
@@ -167,33 +177,11 @@ func registerCloud(
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerLocal(
|
||||
mcpServer *server.MCPServer,
|
||||
cfg *config.Config,
|
||||
) error {
|
||||
interceptor := auth.WithAdminSecret(cfg.Local.AdminSecret)
|
||||
|
||||
localTool := local.NewTool(
|
||||
*cfg.Local.GraphqlURL,
|
||||
*cfg.Local.ConfigServerURL,
|
||||
interceptor,
|
||||
)
|
||||
if err := localTool.Register(mcpServer); err != nil {
|
||||
return fmt.Errorf("failed to register tools: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerProjectTool(
|
||||
mcpServer *server.MCPServer,
|
||||
cfg *config.Config,
|
||||
) error {
|
||||
projectTool, err := project.NewTool(cfg.Projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize tool: %w", err)
|
||||
}
|
||||
|
||||
projectTool := project.NewTool(cfg)
|
||||
if err := projectTool.Register(mcpServer); err != nil {
|
||||
return fmt.Errorf("failed to register tool: %w", err)
|
||||
}
|
||||
|
||||
13
cli/cmd/mcp/testdata/sample.toml
vendored
@@ -1,13 +1,20 @@
|
||||
[cloud]
|
||||
pat = 'your-personal-access-token'
|
||||
enable_mutations = true
|
||||
|
||||
[local]
|
||||
[[projects]]
|
||||
subdomain = 'local'
|
||||
region = 'local'
|
||||
description = 'Local development project running via the Nhost CLI'
|
||||
admin_secret = 'nhost-admin-secret'
|
||||
manage_metadata = true
|
||||
allow_queries = ['*']
|
||||
allow_mutations = ['*']
|
||||
|
||||
[[projects]]
|
||||
subdomain = 'asdasdasdasdasd'
|
||||
region = 'eu-central-1'
|
||||
description = 'Staging project for my awesome app'
|
||||
manage_metadata = false
|
||||
admin_secret = 'your-admin-secret-1'
|
||||
allow_queries = ['*']
|
||||
allow_mutations = ['*']
|
||||
@@ -15,6 +22,8 @@ allow_mutations = ['*']
|
||||
[[projects]]
|
||||
subdomain = 'qweqweqweqweqwe'
|
||||
region = 'us-east-1'
|
||||
description = 'Production project for my awesome app'
|
||||
manage_metadata = false
|
||||
pat = 'pat-for-qweqweqweqweqwe'
|
||||
allow_queries = ['getComments']
|
||||
allow_mutations = ['insertComment', 'updateComment', 'deleteComment']
|
||||
|
||||
@@ -131,7 +131,7 @@ func initInit(
|
||||
|
||||
getclient := &getter.Client{} //nolint:exhaustruct
|
||||
if _, err := getclient.Get(ctx, &getter.Request{ //nolint:exhaustruct
|
||||
Src: "git::https://github.com/nhost/hasura-auth.git//email-templates",
|
||||
Src: "git::https://github.com/nhost/nhost.git//services/auth/email-templates",
|
||||
Dst: "nhost/emails",
|
||||
DisableSymlinks: true,
|
||||
}); err != nil {
|
||||
|
||||
@@ -2,10 +2,13 @@ package software
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/software"
|
||||
@@ -92,8 +95,8 @@ func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
|
||||
|
||||
ce.Infoln("Copying to %s...", curBin)
|
||||
|
||||
if err := os.Rename(tmpFile, curBin); err != nil {
|
||||
return fmt.Errorf("failed to rename %s to %s: %w", tmpFile, curBin, err)
|
||||
if err := moveOrCopyFile(tmpFile, curBin); err != nil {
|
||||
return fmt.Errorf("failed to move %s to %s: %w", tmpFile, curBin, err)
|
||||
}
|
||||
|
||||
ce.Infoln("Setting permissions...")
|
||||
@@ -104,3 +107,55 @@ func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveOrCopyFile(src, dst string) error {
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
var linkErr *os.LinkError
|
||||
// this happens when moving across different filesystems
|
||||
if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EXDEV) {
|
||||
if err := hardMove(src, dst); err != nil {
|
||||
return fmt.Errorf("failed to hard move: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to rename: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hardMove(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return fmt.Errorf("failed to copy file contents: %w", err)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat source file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set file permissions: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(src); err != nil {
|
||||
return fmt.Errorf("failed to remove source file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func auth( //nolint:funlen
|
||||
&model.ConfigSmtp{
|
||||
User: "user",
|
||||
Password: "password",
|
||||
Sender: "hasura-auth@example.com",
|
||||
Sender: "auth@example.com",
|
||||
Host: "mailhog",
|
||||
Port: 1025, //nolint:mnd
|
||||
Secure: false,
|
||||
@@ -56,6 +56,7 @@ func auth( //nolint:funlen
|
||||
false,
|
||||
false,
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get hasura env vars: %w", err)
|
||||
|
||||
@@ -33,6 +33,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_DISABLE_SIGNUP": "false",
|
||||
"AUTH_EMAIL_PASSWORDLESS_ENABLED": "true",
|
||||
"AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED": "true",
|
||||
"AUTH_ENCRYPTION_KEY": "5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
|
||||
"AUTH_GRAVATAR_DEFAULT": "gravatarDefault",
|
||||
"AUTH_GRAVATAR_ENABLED": "true",
|
||||
"AUTH_GRAVATAR_RATING": "gravatarRating",
|
||||
@@ -52,6 +53,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_APPLE_ENABLED": "true",
|
||||
"AUTH_PROVIDER_APPLE_KEY_ID": "appleKeyId",
|
||||
"AUTH_PROVIDER_APPLE_PRIVATE_KEY": "applePrivateKey",
|
||||
"AUTH_PROVIDER_APPLE_SCOPE": "",
|
||||
"AUTH_PROVIDER_APPLE_TEAM_ID": "appleTeamId",
|
||||
"AUTH_PROVIDER_AZUREAD_CLIENT_ID": "azureadClientId",
|
||||
"AUTH_PROVIDER_AZUREAD_CLIENT_SECRET": "azureadClientSecret",
|
||||
@@ -74,9 +76,12 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET": "facebookClientSecret",
|
||||
"AUTH_PROVIDER_FACEBOOK_ENABLED": "true",
|
||||
"AUTH_PROVIDER_FACEBOOK_SCOPE": "email",
|
||||
"AUTH_PROVIDER_GITHUB_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_GITHUB_CLIENT_ID": "githubClientId",
|
||||
"AUTH_PROVIDER_GITHUB_CLIENT_SECRET": "githubClientSecret",
|
||||
"AUTH_PROVIDER_GITHUB_ENABLED": "true",
|
||||
"AUTH_PROVIDER_GITHUB_SCOPE": "user:email",
|
||||
"AUTH_PROVIDER_GITLAB_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_GITLAB_CLIENT_ID": "gitlabClientId",
|
||||
"AUTH_PROVIDER_GITLAB_CLIENT_SECRET": "gitlabClientSecret",
|
||||
"AUTH_PROVIDER_GITLAB_ENABLED": "true",
|
||||
@@ -96,6 +101,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET": "spotifyClientSecret",
|
||||
"AUTH_PROVIDER_SPOTIFY_ENABLED": "true",
|
||||
"AUTH_PROVIDER_SPOTIFY_SCOPE": "user-read-email",
|
||||
"AUTH_PROVIDER_STRAVA_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_STRAVA_CLIENT_ID": "stravaClientId",
|
||||
"AUTH_PROVIDER_STRAVA_CLIENT_SECRET": "stravaClientSecret",
|
||||
"AUTH_PROVIDER_STRAVA_ENABLED": "true",
|
||||
@@ -143,7 +149,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_SMTP_PASS": "password",
|
||||
"AUTH_SMTP_PORT": "1025",
|
||||
"AUTH_SMTP_SECURE": "false",
|
||||
"AUTH_SMTP_SENDER": "hasura-auth@example.com",
|
||||
"AUTH_SMTP_SENDER": "auth@example.com",
|
||||
"AUTH_SMTP_USER": "user",
|
||||
"AUTH_USER_DEFAULT_ALLOWED_ROLES": "user,admin",
|
||||
"AUTH_USER_DEFAULT_ROLE": "user",
|
||||
|
||||
@@ -344,7 +344,7 @@ func dashboard(
|
||||
subdomain, "hasura", httpPort, useTLS,
|
||||
) + "/console",
|
||||
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL": URL(
|
||||
subdomain, "hasura", httpPort, useTLS),
|
||||
subdomain, "hasura", httpPort, useTLS) + "/apis/migrate",
|
||||
"NEXT_PUBLIC_NHOST_STORAGE_URL": URL(
|
||||
subdomain, "storage", httpPort, useTLS) + "/v1",
|
||||
},
|
||||
@@ -459,7 +459,7 @@ func mailhog(subdomain, volumeName string, useTLS bool) *Service {
|
||||
"SMTP_PASS": "password",
|
||||
"SMTP_PORT": "1025",
|
||||
"SMTP_SECURE": "false",
|
||||
"SMTP_SENDER": "hasura-auth@example.com",
|
||||
"SMTP_SENDER": "auth@example.com",
|
||||
"SMTP_USER": "user",
|
||||
},
|
||||
ExtraHosts: extraHosts(subdomain),
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Configuration
|
||||
|
||||
This document describes all available configuration options for the Nhost MCP tool. The configuration file uses TOML format.
|
||||
|
||||
## TOML
|
||||
|
||||
```toml
|
||||
# Cloud configuration for managing Nhost Cloud projects and organizations
|
||||
# Remove section to disable this access
|
||||
[cloud]
|
||||
# Personal Access Token (PAT) for Nhost Cloud API authentication
|
||||
# Get one at: https://app.nhost.io/account
|
||||
pat = "your-pat-here"
|
||||
|
||||
# Enable mutations on Nhost Cloud configurations
|
||||
# When false, only queries are allowed
|
||||
enable_mutations = true
|
||||
|
||||
# Local configuration for interacting with Nhost CLI projects
|
||||
# Remove section to disable access
|
||||
[local]
|
||||
# Admin secret for local project authentication
|
||||
admin_secret = "your-admin-secret"
|
||||
|
||||
# Optional: Custom config server URL
|
||||
# Default: https://local.dashboard.local.nhost.run/v1/configserver/graphql
|
||||
config_server_url = "your-custom-url"
|
||||
|
||||
# Optional: Custom GraphQL URL
|
||||
# Default: https://local.graphql.local.nhost.run/v1
|
||||
graphql_url = "your-custom-url"
|
||||
|
||||
# Project-specific configurations
|
||||
[[projects]]
|
||||
# Project subdomain (required)
|
||||
subdomain = "your-project-subdomain"
|
||||
|
||||
# Project region (required)
|
||||
region = "your-project-region"
|
||||
|
||||
# Authentication: Use either admin_secret or pat
|
||||
# Admin secret for project access
|
||||
admin_secret = "your-project-admin-secret"
|
||||
# OR
|
||||
# Project-specific PAT
|
||||
pat = "your-project-pat"
|
||||
|
||||
# List of allowed GraphQL queries
|
||||
# Use ["*"] to allow all queries, [] to disable all
|
||||
allow_queries = ["*"]
|
||||
|
||||
# List of allowed GraphQL mutations
|
||||
# Use ["*"] to allow all mutations, [] to disable all
|
||||
# Only effective if mutations are enabled for the project
|
||||
allow_mutations = ["*"]
|
||||
```
|
||||
## Example Configuration
|
||||
|
||||
```toml
|
||||
[cloud]
|
||||
pat = "1234567890abcdef"
|
||||
enable_mutations = true
|
||||
|
||||
[local]
|
||||
admin_secret = "nhost-admin-secret"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "my-app"
|
||||
region = "eu-central-1"
|
||||
admin_secret = "project-admin-secret"
|
||||
allow_queries = ["*"]
|
||||
allow_mutations = ["createUser", "updateUser"]
|
||||
|
||||
[[projects]]
|
||||
subdomain = "another-app"
|
||||
region = "us-east-1"
|
||||
pat = "nhp_project_specific_pat"
|
||||
allow_queries = ["getUsers", "getPosts"]
|
||||
allow_mutations = []
|
||||
```
|
||||
@@ -1,96 +0,0 @@
|
||||
# NAME
|
||||
|
||||
nhost-mcp - Nhost's Model Context Protocol (MCP) server
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
nhost-mcp
|
||||
|
||||
```
|
||||
[--help|-h]
|
||||
[--version|-v]
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
|
||||
```
|
||||
nhost-mcp [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...]
|
||||
```
|
||||
|
||||
# GLOBAL OPTIONS
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
**--version, -v**: print the version
|
||||
|
||||
|
||||
# COMMANDS
|
||||
|
||||
## docs
|
||||
|
||||
Generate markdown documentation for the CLI
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
### help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
## config
|
||||
|
||||
Generate and save configuration file
|
||||
|
||||
**--config-file**="": Configuration file path (default: /Users/dbarroso/.config/nhost/mcp-nhost.toml)
|
||||
|
||||
**--confirm**: Skip confirmation prompt
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
### help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
## start
|
||||
|
||||
Starts the MCP server
|
||||
|
||||
**--bind**="": Bind address in the form <host>:<port>. If omitted use stdio
|
||||
|
||||
**--config-file**="": Path to the config file (default: /Users/dbarroso/.config/nhost/mcp-nhost.toml)
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
### help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
## gen
|
||||
|
||||
Generate GraphQL schema for Nhost Cloud
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
**--nhost-pat**="": Personal Access Token
|
||||
|
||||
**--with-mutations**: Include mutations in the generated schema
|
||||
|
||||
### help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
## upgrade
|
||||
|
||||
Checks if there is a new version and upgrades it
|
||||
|
||||
**--confirm**: Confirm the upgrade without prompting
|
||||
|
||||
**--help, -h**: show help
|
||||
|
||||
### help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
## help, h
|
||||
|
||||
Shows a list of commands or help for one command
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Screenshots
|
||||
|
||||
Listing cloud projects:
|
||||
|
||||
<img src="screenshots/101-cloud-projects.png" width="600" alt="listing cloud projects">
|
||||
|
||||
Changing cloud project's configuration:
|
||||
|
||||
<img src="screenshots/102-cloud-project-config.png" width="600" alt="changing cloud project's configuration">
|
||||
|
||||
Querying cloud project's configuration:
|
||||
|
||||
<img src="screenshots/103-cloud-project-config2.png" width="600" alt="querying cloud project's configuration">
|
||||
|
||||
Querying local project's schema:
|
||||
|
||||
<img src="screenshots/201-local-schema.png" width="600" alt="querying local project's schema">
|
||||
|
||||
Generating code from local project's schema:
|
||||
|
||||
<img src="screenshots/202-local-code.png" alt="generating code from local project's schema">
|
||||
|
||||
Resulting code:
|
||||
|
||||
<img src="screenshots/203-result.png" alt="resulting code">
|
||||
|
||||
Querying local project's configuration:
|
||||
|
||||
<img src="screenshots/204-local-config-query.png" width="600" alt="querying local project's configuration">
|
||||
|
||||
Modifying local project's configuration:
|
||||
|
||||
<img src="screenshots/205-local-config-change.png" width="600" alt="modifying local project's configuration">
|
||||
|
||||
Querying cloud project's schema:
|
||||
|
||||
<img src="screenshots/301-project-schema.png" width="600" alt="project schema">
|
||||
|
||||
Querying cloud project's data:
|
||||
|
||||
<img src="screenshots/302-project-query.png" width="600" alt="project data">
|
||||
|
||||
Managing cloud project's data:
|
||||
|
||||
<img src="screenshots/303-project-mutation.png" width="600" alt="project mutation">
|
||||
|
||||
Analysing cloud project's data:
|
||||
|
||||
<img src="screenshots/304-project-data-analysis.png" width="600" alt="project data analysis">
|
||||
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 819 KiB |
|
Before Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 181 KiB |
@@ -3,6 +3,10 @@
|
||||
name = 'GREET'
|
||||
value = 'Sayonara'
|
||||
|
||||
[[global.environment]]
|
||||
name = 'NODE_ENV'
|
||||
value = 'production'
|
||||
|
||||
[hasura]
|
||||
version = 'v2.46.0-ce'
|
||||
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
||||
@@ -63,11 +67,6 @@ default = "00000000-0000-0000-0000-000000000000"
|
||||
expiresIn = 2592000
|
||||
|
||||
[auth.method]
|
||||
[auth.method.anonymous]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPasswordless]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPassword]
|
||||
hibpEnabled = false
|
||||
@@ -139,46 +138,11 @@ version = '14.18-20250728-1'
|
||||
[postgres.resources.storage]
|
||||
capacity = 1
|
||||
|
||||
[postgres.settings]
|
||||
maxConnections = 100
|
||||
sharedBuffers = '256MB'
|
||||
effectiveCacheSize = '768MB'
|
||||
maintenanceWorkMem = '64MB'
|
||||
checkpointCompletionTarget = 0.9
|
||||
walBuffers = '-1'
|
||||
defaultStatisticsTarget = 100
|
||||
randomPageCost = 1.1
|
||||
effectiveIOConcurrency = 200
|
||||
workMem = '1310kB'
|
||||
hugePages = 'off'
|
||||
minWalSize = '80MB'
|
||||
maxWalSize = '1GB'
|
||||
maxWorkerProcesses = 8
|
||||
maxParallelWorkersPerGather = 2
|
||||
maxParallelWorkers = 8
|
||||
maxParallelMaintenanceWorkers = 2
|
||||
|
||||
[provider]
|
||||
|
||||
[storage]
|
||||
version = '0.7.1'
|
||||
|
||||
[ai]
|
||||
version = '0.8.0'
|
||||
webhookSecret = '{{ secrets.GRAPHITE_WEBHOOK_SECRET }}'
|
||||
|
||||
[ai.resources]
|
||||
[ai.resources.compute]
|
||||
cpu = 125
|
||||
memory = 256
|
||||
|
||||
[ai.openai]
|
||||
organization = ''
|
||||
apiKey = '{{ secrets.OPENAI_API_KEY }}'
|
||||
|
||||
[ai.autoEmbeddings]
|
||||
synchPeriodMinutes = 5
|
||||
|
||||
[observability]
|
||||
[observability.grafana]
|
||||
adminPassword = '{{ secrets.GRAFANA_ADMIN_PASSWORD }}'
|
||||
|
||||
@@ -44,7 +44,7 @@ if [[ "$version" == "latest" ]]; then
|
||||
release=$(curl --silent https://api.github.com/repos/nhost/nhost/releases\?per_page=100 | grep tag_name | grep \"cli\@ | head -n 1 | sed 's/.*"tag_name": "\([^"]*\)".*/\1/')
|
||||
version=$( echo $release | sed 's/.*@//')
|
||||
else
|
||||
release="cli@$release"
|
||||
release="cli@$version"
|
||||
fi
|
||||
|
||||
# check version exists
|
||||
|
||||
@@ -1,58 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultLocalConfigServerURL = "https://local.dashboard.local.nhost.run/v1/configserver/graphql"
|
||||
DefaultLocalGraphqlURL = "https://local.graphql.local.nhost.run/v1"
|
||||
)
|
||||
|
||||
var ErrProjectNotConfigured = errors.New("project not configured")
|
||||
|
||||
type Config struct {
|
||||
// If configured allows managing the cloud. For instance, this allows you to configure
|
||||
// projects, list projects, organizations, and so on.
|
||||
Cloud *Cloud `json:"cloud,omitempty" toml:"cloud"`
|
||||
|
||||
// If configured allows working with a local project running via the CLI. This includes
|
||||
// configuring it, working with the schema, migrations, etc.
|
||||
Local *Local `json:"local,omitempty" toml:"local"`
|
||||
|
||||
// Projects is a list of projects that you want to allow access to. This grants access to the
|
||||
// GraphQL schema allowing it to inspect it and run allowed queries and mutations.
|
||||
Projects []Project `json:"projects" toml:"projects"`
|
||||
// Projects is a list of projects that you want to allow access to. This grants access
|
||||
// to the GraphQL schema allowing it to inspect it and run allowed queries and mutations.
|
||||
Projects ProjectList `json:"projects" toml:"projects"`
|
||||
}
|
||||
|
||||
type Cloud struct {
|
||||
// Personal Access Token to authenticate with the Nhost Cloud API. You can get one
|
||||
// on the following URL: https://app.nhost.io/account
|
||||
PAT string `json:"pat" toml:"pat"`
|
||||
|
||||
// If enabled you can run mutations against the Nhost Cloud to manipulate project's configurations
|
||||
// amongst other things. Queries are always allowed if this section is configured.
|
||||
EnableMutations bool `json:"enable_mutations" toml:"enable_mutations"`
|
||||
}
|
||||
|
||||
type Local struct {
|
||||
// Admin secret to use when running against a local project.
|
||||
AdminSecret string `json:"admin_secret" toml:"admin_secret"`
|
||||
type ProjectList []Project
|
||||
|
||||
// GraphQL URL to use when running against a local project.
|
||||
// Defaults to "https://local.dashboard.local.nhost.run/v1/configserver/graphql"
|
||||
ConfigServerURL *string `json:"config_server_url,omitempty" toml:"config_server_url,omitempty"`
|
||||
func (pl ProjectList) Get(subdomain string) (*Project, error) {
|
||||
for _, p := range pl {
|
||||
if p.Subdomain == subdomain {
|
||||
return &p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQL URL to use when running against a local project.
|
||||
// Defaults to "https://local.graphql.local.nhost.run/v1"
|
||||
GraphqlURL *string `json:"graphql_url,omitempty" toml:"graphql_url,omitempty"`
|
||||
return nil, fmt.Errorf("%w: %s", ErrProjectNotConfigured, subdomain)
|
||||
}
|
||||
|
||||
func (pl ProjectList) Subdomains() []string {
|
||||
subdomains := make([]string, 0, len(pl))
|
||||
|
||||
for _, p := range pl {
|
||||
subdomains = append(subdomains, p.Subdomain)
|
||||
}
|
||||
|
||||
return subdomains
|
||||
}
|
||||
|
||||
func (pl ProjectList) Instructions() string {
|
||||
if len(pl) == 0 {
|
||||
return "No projects configured. Please, run `nhost mcp config` to configure your projects."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Configured projects:\n")
|
||||
|
||||
for _, p := range pl {
|
||||
sb.WriteString(fmt.Sprintf("- %s (%s): %s\n", p.Subdomain, p.Region, p.Description))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
@@ -62,6 +82,9 @@ type Project struct {
|
||||
// Project's region
|
||||
Region string `json:"region" toml:"region"`
|
||||
|
||||
// Project's description
|
||||
Description string `json:"description,omitempty" toml:"description,omitempty"`
|
||||
|
||||
// Admin secret to operate against the project.
|
||||
// Either admin secret or PAT is required.
|
||||
AdminSecret *string `json:"admin_secret,omitempty" toml:"admin_secret,omitempty"`
|
||||
@@ -70,6 +93,10 @@ type Project struct {
|
||||
// Either admin secret or PAT is required.
|
||||
PAT *string `json:"pat,omitempty" toml:"pat,omitempty"`
|
||||
|
||||
// If enabled, allows managing the project's metadata (tables, relationships,
|
||||
// permissions, etc).
|
||||
ManageMetadata bool `json:"manage_metadata,omitempty" toml:"manage_metadata,omitempty"`
|
||||
|
||||
// List of queries that are allowed to be executed against the project.
|
||||
// If empty, no queries are allowed. Use [*] to allow all queries.
|
||||
AllowQueries []string `json:"allow_queries" toml:"allow_queries"`
|
||||
@@ -78,30 +105,80 @@ type Project struct {
|
||||
// If empty, no mutations are allowed. Use [*] to allow all mutations.
|
||||
// Note that this is only used if the project is configured to allow mutations.
|
||||
AllowMutations []string `json:"allow_mutations" toml:"allow_mutations"`
|
||||
|
||||
// GraphQL URL to use when running against the project. Defaults to constructed URL with
|
||||
// the subdomain and region.
|
||||
GraphqlURL string `json:"graphql_url,omitzero" toml:"graphql_url,omitzero"`
|
||||
|
||||
// Auth URL to use when running against the project. Defaults to constructed URL with
|
||||
// the subdomain and region.
|
||||
AuthURL string `json:"auth_url,omitzero" toml:"auth_url,omitzero"`
|
||||
|
||||
// Hasura's base URL. Defaults to constructed URL with the subdomain and region.
|
||||
HasuraURL string `json:"hasura_url,omitzero" toml:"hasura_url,omitzero"`
|
||||
}
|
||||
|
||||
func GetConfigPath() string {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "mcp-nhost.toml"
|
||||
}
|
||||
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
func (p *Project) GetAuthURL() string {
|
||||
if p.AuthURL != "" {
|
||||
return p.AuthURL
|
||||
}
|
||||
|
||||
return filepath.Join(configHome, "nhost", "mcp-nhost.toml")
|
||||
return fmt.Sprintf("https://%s.auth.%s.nhost.run/v1", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetGraphqlURL() string {
|
||||
if p.GraphqlURL != "" {
|
||||
return p.GraphqlURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.graphql.%s.nhost.run/v1", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetHasuraURL() string {
|
||||
if p.HasuraURL != "" {
|
||||
return p.HasuraURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.hasura.%s.nhost.run", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetAuthInterceptor() (func(ctx context.Context, req *http.Request) error, error) {
|
||||
if p.AdminSecret != nil {
|
||||
return auth.WithAdminSecret(*p.AdminSecret), nil
|
||||
} else if p.PAT != nil {
|
||||
interceptor, err := auth.WithPAT(p.GetAuthURL(), *p.PAT)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PAT interceptor: %w", err)
|
||||
}
|
||||
|
||||
return interceptor, nil
|
||||
}
|
||||
|
||||
return func(_ context.Context, _ *http.Request) error {
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetConfigPath(cmd *cli.Command) string {
|
||||
configPath := cmd.String("config-file")
|
||||
if configPath != "" {
|
||||
return configPath
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
return filepath.Join(ce.Path.DotNhostFolder(), "mcp-nhost.toml")
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0o600) //nolint:mnd
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open config file: %w", err)
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
decoder := toml.NewDecoder(f)
|
||||
interpolated := interpolateEnv(string(content), os.Getenv)
|
||||
|
||||
decoder := toml.NewDecoder(strings.NewReader(interpolated))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
var config Config
|
||||
@@ -120,15 +197,5 @@ func Load(path string) (*Config, error) {
|
||||
return nil, fmt.Errorf("failed to unmarshal config file: %w", err)
|
||||
}
|
||||
|
||||
if config.Local != nil {
|
||||
if config.Local.GraphqlURL == nil {
|
||||
config.Local.GraphqlURL = ptr(DefaultLocalGraphqlURL)
|
||||
}
|
||||
|
||||
if config.Local.ConfigServerURL == nil {
|
||||
config.Local.ConfigServerURL = ptr(DefaultLocalConfigServerURL)
|
||||
}
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
60
cli/mcp/config/interpolate.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// interpolateEnv replaces environment variables in the format $VAR.
|
||||
// Supports escaping $ with $$ or \$.
|
||||
func interpolateEnv(s string, getenv func(string) string) string { //nolint:cyclop
|
||||
var result strings.Builder
|
||||
result.Grow(len(s))
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch {
|
||||
case s[i] == '\\' && i+1 < len(s) && s[i+1] == '$':
|
||||
// Handle \$ escape sequence
|
||||
result.WriteByte('$')
|
||||
|
||||
i++ // skip the $
|
||||
case s[i] == '$' && i+1 < len(s) && s[i+1] == '$':
|
||||
// Handle $$ escape sequence
|
||||
result.WriteByte('$')
|
||||
|
||||
i++ // skip the second $
|
||||
case s[i] == '$':
|
||||
// Start of variable substitution
|
||||
i++
|
||||
if i >= len(s) {
|
||||
result.WriteByte('$')
|
||||
break
|
||||
}
|
||||
|
||||
// Extract variable name
|
||||
start := i
|
||||
for i < len(s) && (isAlphaNumUnderscore(s[i])) {
|
||||
i++
|
||||
}
|
||||
|
||||
if i == start {
|
||||
// No valid variable name found
|
||||
result.WriteByte('$')
|
||||
|
||||
i--
|
||||
} else {
|
||||
varName := s[start:i]
|
||||
if value := getenv(varName); value != "" {
|
||||
result.WriteString(value)
|
||||
}
|
||||
|
||||
i-- // Back up one because the loop will increment
|
||||
}
|
||||
default:
|
||||
result.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func isAlphaNumUnderscore(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
||||
}
|
||||
256
cli/mcp/config/interpolate_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package config //nolint:testpackage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestInterpolateEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple variable substitution",
|
||||
input: "admin_secret = \"$SECRET\"",
|
||||
envVars: map[string]string{"SECRET": "mysecret"},
|
||||
expected: "admin_secret = \"mysecret\"",
|
||||
},
|
||||
{
|
||||
name: "multiple variables",
|
||||
input: "$VAR1 and $VAR2",
|
||||
envVars: map[string]string{"VAR1": "hello", "VAR2": "world"},
|
||||
expected: "hello and world",
|
||||
},
|
||||
{
|
||||
name: "variable with underscores",
|
||||
input: "$MY_VAR_123",
|
||||
envVars: map[string]string{"MY_VAR_123": "value"},
|
||||
expected: "value",
|
||||
},
|
||||
{
|
||||
name: "escaped with $$",
|
||||
input: "price = $$100",
|
||||
envVars: map[string]string{},
|
||||
expected: "price = $100",
|
||||
},
|
||||
{
|
||||
name: "escaped with backslash",
|
||||
input: "price = \\$100",
|
||||
envVars: map[string]string{},
|
||||
expected: "price = $100",
|
||||
},
|
||||
{
|
||||
name: "mix of escaped and variable",
|
||||
input: "$$SECRET is $SECRET",
|
||||
envVars: map[string]string{"SECRET": "hidden"},
|
||||
expected: "$SECRET is hidden",
|
||||
},
|
||||
{
|
||||
name: "undefined variable",
|
||||
input: "value = $UNDEFINED",
|
||||
envVars: map[string]string{},
|
||||
expected: "value = ",
|
||||
},
|
||||
{
|
||||
name: "variable at end",
|
||||
input: "end$VAR",
|
||||
envVars: map[string]string{"VAR": "value"},
|
||||
expected: "endvalue",
|
||||
},
|
||||
{
|
||||
name: "dollar sign alone at end",
|
||||
input: "end$",
|
||||
envVars: map[string]string{},
|
||||
expected: "end$",
|
||||
},
|
||||
{
|
||||
name: "dollar sign with non-alphanum",
|
||||
input: "$ hello",
|
||||
envVars: map[string]string{},
|
||||
expected: "$ hello",
|
||||
},
|
||||
{
|
||||
name: "no variables",
|
||||
input: "plain text without variables",
|
||||
envVars: map[string]string{},
|
||||
expected: "plain text without variables",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
envVars: map[string]string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "multiple escapes in a row",
|
||||
input: "$$$$",
|
||||
envVars: map[string]string{},
|
||||
expected: "$$",
|
||||
},
|
||||
{
|
||||
name: "variable surrounded by text",
|
||||
input: "prefix$VAR suffix",
|
||||
envVars: map[string]string{"VAR": "middle"},
|
||||
expected: "prefixmiddle suffix",
|
||||
},
|
||||
{
|
||||
name: "backslash escape followed by variable",
|
||||
input: "\\$100 costs $PRICE",
|
||||
envVars: map[string]string{"PRICE": "$50"},
|
||||
expected: "$100 costs $50",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create isolated getenv function
|
||||
getenv := func(key string) string {
|
||||
return tt.envVars[key]
|
||||
}
|
||||
|
||||
result := interpolateEnv(tt.input, getenv)
|
||||
if result != tt.expected {
|
||||
t.Errorf("interpolateEnv() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateEnvRealWorld(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
envVars := map[string]string{
|
||||
"ADMIN_SECRET": "super-secret-key",
|
||||
"SUBDOMAIN": "myapp",
|
||||
}
|
||||
getenv := func(key string) string {
|
||||
return envVars[key]
|
||||
}
|
||||
|
||||
input := `[[projects]]
|
||||
subdomain = "local"
|
||||
region = "local"
|
||||
admin_secret = "$ADMIN_SECRET"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "$SUBDOMAIN"
|
||||
admin_secret = "$ADMIN_SECRET"
|
||||
# Price is $$100
|
||||
`
|
||||
|
||||
expected := `[[projects]]
|
||||
subdomain = "local"
|
||||
region = "local"
|
||||
admin_secret = "super-secret-key"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "myapp"
|
||||
admin_secret = "super-secret-key"
|
||||
# Price is $100
|
||||
`
|
||||
|
||||
result := interpolateEnv(input, getenv)
|
||||
if result != expected {
|
||||
t.Errorf("interpolateEnv() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAlphaNumUnderscore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
char byte
|
||||
expected bool
|
||||
}{
|
||||
{'a', true},
|
||||
{'z', true},
|
||||
{'A', true},
|
||||
{'Z', true},
|
||||
{'0', true},
|
||||
{'9', true},
|
||||
{'_', true},
|
||||
{'-', false},
|
||||
{'.', false},
|
||||
{'$', false},
|
||||
{' ', false},
|
||||
{'/', false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.char), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := isAlphaNumUnderscore(tt.char)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAlphaNumUnderscore(%q) = %v, want %v", tt.char, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestLoadWithInterpolation(t *testing.T) {
|
||||
// Create a temporary config file
|
||||
content := `[[projects]]
|
||||
admin_secret = "$TEST_ADMIN_SECRET"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "myapp"
|
||||
region = "us-east-1"
|
||||
admin_secret = "$TEST_PROJECT_SECRET"
|
||||
allow_queries = ["*"]
|
||||
`
|
||||
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "config-*.toml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
t.Setenv("TEST_ADMIN_SECRET", "local-secret")
|
||||
t.Setenv("TEST_PROJECT_SECRET", "project-secret")
|
||||
|
||||
// Load config
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(cfg, &Config{
|
||||
Cloud: nil,
|
||||
Projects: ProjectList{
|
||||
{ //nolint:exhaustruct
|
||||
AdminSecret: ptr("local-secret"),
|
||||
},
|
||||
{ //nolint:exhaustruct
|
||||
Subdomain: "myapp",
|
||||
Region: "us-east-1",
|
||||
AdminSecret: ptr("project-secret"),
|
||||
AllowQueries: []string{"*"},
|
||||
},
|
||||
},
|
||||
}); diff != "" {
|
||||
t.Errorf("diff = %s", diff)
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,14 @@ func RunWizard() (*Config, error) {
|
||||
|
||||
projects := wizardProject(reader)
|
||||
|
||||
if localConfig != nil {
|
||||
projects = append(projects, *localConfig)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
return &Config{
|
||||
Cloud: cloudConfig,
|
||||
Local: localConfig,
|
||||
Projects: projects,
|
||||
}, nil
|
||||
}
|
||||
@@ -41,13 +44,9 @@ func wizardCloud(reader *bufio.Reader) *Cloud {
|
||||
fmt.Println(" You can view and configure projects as you would in the dashboard.")
|
||||
|
||||
if promptYesNo(reader, "Enable Nhost Cloud access?") {
|
||||
pat := promptString(
|
||||
reader,
|
||||
"Enter Personal Access Token (from https://app.nhost.io/account):",
|
||||
)
|
||||
fmt.Println(" Note: If you haven't already, run `nhost login` to authenticate.")
|
||||
|
||||
return &Cloud{
|
||||
PAT: pat,
|
||||
EnableMutations: true,
|
||||
}
|
||||
}
|
||||
@@ -56,7 +55,7 @@ func wizardCloud(reader *bufio.Reader) *Cloud {
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func wizardLocal(reader *bufio.Reader) *Local {
|
||||
func wizardLocal(reader *bufio.Reader) *Project {
|
||||
fmt.Println("2. Local Development Access")
|
||||
fmt.Println(" This allows LLMs to interact with your local Nhost environment,")
|
||||
fmt.Println(" including project configuration and GraphQL API access.")
|
||||
@@ -68,10 +67,18 @@ func wizardLocal(reader *bufio.Reader) *Local {
|
||||
adminSecret = "nhost-admin-secret" //nolint:gosec
|
||||
}
|
||||
|
||||
return &Local{
|
||||
AdminSecret: adminSecret,
|
||||
ConfigServerURL: nil,
|
||||
GraphqlURL: nil,
|
||||
return &Project{
|
||||
Subdomain: "local",
|
||||
Region: "local",
|
||||
Description: "Local development project running via the Nhost CLI",
|
||||
AdminSecret: &adminSecret,
|
||||
PAT: nil,
|
||||
ManageMetadata: true,
|
||||
AllowQueries: []string{"*"},
|
||||
AllowMutations: []string{"*"},
|
||||
AuthURL: "",
|
||||
GraphqlURL: "",
|
||||
HasuraURL: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +102,29 @@ func wizardProject(reader *bufio.Reader) []Project {
|
||||
if promptYesNo(reader, "Configure project access?") {
|
||||
for {
|
||||
project := Project{
|
||||
Description: "",
|
||||
Subdomain: "",
|
||||
Region: "",
|
||||
AdminSecret: nil,
|
||||
PAT: nil,
|
||||
ManageMetadata: false,
|
||||
AllowQueries: []string{"*"},
|
||||
AllowMutations: []string{"*"},
|
||||
GraphqlURL: "",
|
||||
AuthURL: "",
|
||||
HasuraURL: "",
|
||||
}
|
||||
|
||||
project.Subdomain = promptString(reader, "Project subdomain:")
|
||||
project.Region = promptString(reader, "Project region:")
|
||||
project.Description = promptString(
|
||||
reader,
|
||||
"Project description to provide additional information to LLMs:",
|
||||
)
|
||||
project.ManageMetadata = promptYesNo(
|
||||
reader,
|
||||
"Allow managing metadata (tables, relationships, permissions, etc)?",
|
||||
)
|
||||
|
||||
authType := promptChoice(
|
||||
reader,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -30,7 +31,7 @@ func getTypeName(t Type) string {
|
||||
}
|
||||
|
||||
// ParseSchema converts an introspection query result into a GraphQL SDL string.
|
||||
func ParseSchema(response ResponseIntrospection, filter Filter) string {
|
||||
func ParseSchema(response ResponseIntrospection, filter Filter) string { //nolint:cyclop
|
||||
availableTypes := make(map[string]Type)
|
||||
|
||||
// Process all types in the schema
|
||||
@@ -58,6 +59,9 @@ func ParseSchema(response ResponseIntrospection, filter Filter) string {
|
||||
}
|
||||
|
||||
neededMutations := make(map[string]Field)
|
||||
if response.Data.Schema.MutationType == nil {
|
||||
return render(neededQueries, neededMutations, neededTypes)
|
||||
}
|
||||
|
||||
for _, mutation := range response.Data.Schema.MutationType.Fields {
|
||||
if filter.AllowMutations == nil {
|
||||
@@ -83,6 +87,30 @@ func ParseSchema(response ResponseIntrospection, filter Filter) string {
|
||||
return render(neededQueries, neededMutations, neededTypes)
|
||||
}
|
||||
|
||||
func SummarizeSchema(response ResponseIntrospection) string {
|
||||
summary := map[string][]string{
|
||||
"query": make([]string, len(response.Data.Schema.QueryType.Fields)),
|
||||
}
|
||||
|
||||
for i, query := range response.Data.Schema.QueryType.Fields {
|
||||
summary["query"][i] = query.Name
|
||||
}
|
||||
|
||||
if response.Data.Schema.MutationType != nil {
|
||||
summary["mutation"] = make([]string, len(response.Data.Schema.MutationType.Fields))
|
||||
for _, mutation := range response.Data.Schema.MutationType.Fields {
|
||||
summary["mutation"] = append(summary["mutation"], mutation.Name)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(summary, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("failed to marshal summary: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func filterNestedArgs(
|
||||
args []InputValue, neededTypes map[string]Type,
|
||||
) []InputValue {
|
||||
|
||||
@@ -24,6 +24,10 @@ func checkAllowedOperation(
|
||||
selectionSet ast.SelectionSet,
|
||||
allowed []string,
|
||||
) error {
|
||||
if slices.Contains(allowed, "*") {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, v := range selectionSet {
|
||||
if v, ok := v.(*ast.Field); ok {
|
||||
if len(v.SelectionSet) > 0 && !slices.Contains(allowed, v.Name) {
|
||||
@@ -45,8 +49,8 @@ func CheckAllowedGraphqlQuery( //nolint:cyclop
|
||||
queryString string,
|
||||
) error {
|
||||
if allowedQueries == nil && allowedMutations == nil {
|
||||
// nil means unrestricted
|
||||
return nil
|
||||
// nil means nothing allowed
|
||||
return fmt.Errorf("%w: %s", ErrQueryNotAllowed, queryString)
|
||||
}
|
||||
|
||||
if len(allowedQueries) == 0 && len(allowedMutations) == 0 {
|
||||
|
||||
@@ -22,7 +22,21 @@ func TestCheckAllowedGraphqlQuery(t *testing.T) {
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: nil,
|
||||
allowedMutations: nil,
|
||||
expectedError: nil,
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "nil,",
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: nil,
|
||||
allowedMutations: []string{"user"},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: ",nil",
|
||||
query: `mutation { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: nil,
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "no query allowed",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package auth provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT.
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package graphql provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT.
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||
package graphql
|
||||
|
||||
import (
|
||||
|
||||
69
cli/mcp/resources/cloud.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
)
|
||||
|
||||
//go:embed cloud_schema.graphql
|
||||
var schemaGraphql string
|
||||
|
||||
//go:embed cloud_schema-with-mutations.graphql
|
||||
var schemaGraphqlWithMutations string
|
||||
|
||||
const (
|
||||
CloudResourceURI = "schema://nhost-cloud"
|
||||
CloudDescription = `Schema to interact with the Nhost Cloud. Projects are equivalent
|
||||
to apps in the schema. IDs are typically uuids.`
|
||||
)
|
||||
|
||||
type Cloud struct {
|
||||
schema string
|
||||
}
|
||||
|
||||
func NewCloud(cfg *config.Config) *Cloud {
|
||||
schema := schemaGraphql
|
||||
if cfg.Cloud.EnableMutations {
|
||||
schema = schemaGraphqlWithMutations
|
||||
}
|
||||
|
||||
return &Cloud{
|
||||
schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Cloud) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: CloudResourceURI,
|
||||
Name: "nhost-cloud",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: CloudDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Cloud) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: t.schema,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
54
cli/mcp/resources/graphql_management_schema.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
GraphqlManagementResourceURI = "schema://graphql-management"
|
||||
GraphqlManagementDescription = `GraphQL's management schema for an Nhost project.
|
||||
This tool is useful to properly understand how manage hasura metadata, migrations,
|
||||
permissions, remote schemas, etc.`
|
||||
)
|
||||
|
||||
type GraphqlManagement struct{}
|
||||
|
||||
func NewGraphqlManagement() *GraphqlManagement {
|
||||
return &GraphqlManagement{}
|
||||
}
|
||||
|
||||
func (t *GraphqlManagement) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: GraphqlManagementResourceURI,
|
||||
Name: "graphql-management",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: GraphqlManagementDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *GraphqlManagement) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: graphql.Schema,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
57
cli/mcp/resources/nhost_toml.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
//go:embed nhost_toml_schema.cue
|
||||
var schemaNhostToml string
|
||||
|
||||
const (
|
||||
NhostTomlResourceURI = "schema://nhost.toml"
|
||||
NhostTomlResourceDescription = `Cuelang schema for the nhost.toml configuration file. Run nhost
|
||||
config validate after making changes to your nhost.toml file to ensure it is valid.`
|
||||
)
|
||||
|
||||
type NhostToml struct{}
|
||||
|
||||
func NewNhostToml() *NhostToml {
|
||||
return &NhostToml{}
|
||||
}
|
||||
|
||||
func (t *NhostToml) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: NhostTomlResourceURI,
|
||||
Name: "nhost.toml",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: NhostTomlResourceDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
//go:generate cp ../../../vendor/github.com/nhost/be/services/mimir/schema/schema.cue nhost_toml_schema.cue
|
||||
func (t *NhostToml) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: schemaNhostToml,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
812
cli/mcp/resources/nhost_toml_schema.cue
Normal file
@@ -0,0 +1,812 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"list"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// main entrypoint to the configuration
|
||||
#Config: {
|
||||
// Global configuration that applies to all services
|
||||
global: #Global
|
||||
|
||||
// Configuration for hasura
|
||||
hasura: #Hasura
|
||||
|
||||
// Advanced configuration for GraphQL
|
||||
graphql?: #Graphql
|
||||
|
||||
// Configuration for functions service
|
||||
functions: #Functions
|
||||
|
||||
// Configuration for auth service
|
||||
auth: #Auth
|
||||
|
||||
// Configuration for postgres service
|
||||
postgres: #Postgres
|
||||
|
||||
// Configuration for third party providers like SMTP, SMS, etc.
|
||||
provider: #Provider
|
||||
|
||||
// Configuration for storage service
|
||||
storage: #Storage
|
||||
|
||||
// Configuration for graphite service
|
||||
ai?: #AI
|
||||
|
||||
// Configuration for observability service
|
||||
observability: #Observability
|
||||
|
||||
_totalResourcesCPU: (
|
||||
hasura.resources.replicas*hasura.resources.compute.cpu +
|
||||
auth.resources.replicas*auth.resources.compute.cpu +
|
||||
storage.resources.replicas*storage.resources.compute.cpu +
|
||||
postgres.resources.compute.cpu) @cuegraph(skip)
|
||||
|
||||
_totalResourcesMemory: (
|
||||
hasura.resources.replicas*hasura.resources.compute.memory +
|
||||
auth.resources.replicas*auth.resources.compute.memory +
|
||||
storage.resources.replicas*storage.resources.compute.memory +
|
||||
postgres.resources.compute.memory) @cuegraph(skip)
|
||||
|
||||
_validateResourcesTotalCpuMemoryRatioMustBe1For2: (
|
||||
_totalResourcesCPU*2.048 & _totalResourcesMemory*1.0) @cuegraph(skip)
|
||||
|
||||
_validateResourcesTotalCpuMin1000: (
|
||||
hasura.resources.compute.cpu+
|
||||
auth.resources.compute.cpu+
|
||||
storage.resources.compute.cpu+
|
||||
postgres.resources.compute.cpu) >= 1000 & true @cuegraph(skip)
|
||||
|
||||
_validateAllResourcesAreSetOrNot: (
|
||||
((hasura.resources.compute != _|_) == (auth.resources.compute != _|_)) &&
|
||||
((auth.resources.compute != _|_) == (storage.resources.compute != _|_)) &&
|
||||
((storage.resources.compute != _|_) == (postgres.resources.compute != _|_))) & true @cuegraph(skip)
|
||||
|
||||
_validateNetworkingMustBeNullOrNotSet: !storage.resources.networking | storage.resources.networking == null @cuegraph(skip)
|
||||
|
||||
_isProviderSMTPSet: provider.smtp != _|_ @cuegraph(skip)
|
||||
_isAuthRateLimitEmailsDefault: auth.rateLimit.emails.limit == 10 && auth.rateLimit.emails.interval == "1h" @cuegraph(skip)
|
||||
_validateAuthRateLimitEmailsIsDefaultOrSMTPSettingsSet: (_isProviderSMTPSet | _isAuthRateLimitEmailsDefault) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Global configuration that applies to all services
|
||||
#Global: {
|
||||
// User-defined environment variables that are spread over all services
|
||||
environment: [...#GlobalEnvironmentVariable] | *[]
|
||||
}
|
||||
|
||||
#GlobalEnvironmentVariable: {
|
||||
// Name of the environment variable
|
||||
name: =~"(?i)^[a-z_]{1,}[a-z0-9_]*" & !~"(?i)^NHOST_" & !~"(?i)^HASURA_"
|
||||
// Value of the environment variable
|
||||
value: string
|
||||
}
|
||||
|
||||
#Graphql: {
|
||||
security: #GraphqlSecurity
|
||||
}
|
||||
|
||||
#GraphqlSecurity: {
|
||||
forbidAminSecret: bool | *false
|
||||
maxDepthQueries: uint | *0 // 0 disables the check
|
||||
}
|
||||
|
||||
#Networking: {
|
||||
ingresses: [#Ingress] | *[]
|
||||
}
|
||||
|
||||
#Ingress: {
|
||||
fqdn: [string & net.FQDN & strings.MinRunes(1) & strings.MaxRunes(63)]
|
||||
|
||||
tls?: {
|
||||
clientCA?: string
|
||||
}
|
||||
}
|
||||
|
||||
#Autoscaler: {
|
||||
maxReplicas: uint8 & >=2 & <=100
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
#Resources: {
|
||||
compute?: #ResourcesCompute
|
||||
|
||||
// Number of replicas for a service
|
||||
replicas: uint8 & >=1 & <=10 | *1
|
||||
autoscaler?: #Autoscaler
|
||||
|
||||
_validateReplicasMustBeSmallerThanMaxReplicas: (replicas <= autoscaler.maxReplicas) & true @cuegraph(skip)
|
||||
|
||||
_validateMultipleReplicasNeedsCompute: (
|
||||
replicas == 1 && autoscaler == _|_ |
|
||||
compute != _|_) & true @cuegraph(skip)
|
||||
_validateMultipleReplicasRatioMustBe1For2: (
|
||||
replicas == 1 && autoscaler == _|_ |
|
||||
(compute.cpu*2.048 == compute.memory)) & true @cuegraph(skip)
|
||||
|
||||
networking?: #Networking | null
|
||||
}
|
||||
|
||||
#ResourcesCompute: {
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
cpu: uint32 & >=250 & <=30000
|
||||
// MiB: 128MiB to 30GiB
|
||||
memory: uint32 & >=128 & <=62464
|
||||
|
||||
// validate CPU steps of 250 milicpus
|
||||
_validateCPUSteps250: (mod(cpu, 250) == 0) & true @cuegraph(skip)
|
||||
|
||||
// validate memory steps of 128 MiB
|
||||
_validateMemorySteps128: (mod(memory, 128) == 0) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Configuration for hasura service
|
||||
#Hasura: {
|
||||
// Version of hasura, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/hasura/graphql-engine/tags
|
||||
version: string | *"v2.48.5-ce"
|
||||
|
||||
// JWT Secrets configuration
|
||||
jwtSecrets: [#JWTSecret]
|
||||
|
||||
// Admin secret
|
||||
adminSecret: string
|
||||
|
||||
// Webhook secret
|
||||
webhookSecret: string
|
||||
|
||||
// Configuration for hasura services
|
||||
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
|
||||
settings: {
|
||||
// HASURA_GRAPHQL_CORS_DOMAIN
|
||||
corsDomain: [...#Url] | *["*"]
|
||||
// HASURA_GRAPHQL_DEV_MODE
|
||||
devMode: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_ALLOWLIST
|
||||
enableAllowList: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLE_CONSOLE
|
||||
enableConsole: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
|
||||
enableRemoteSchemaPermissions: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLED_APIS
|
||||
enabledAPIs: [...#HasuraAPIs] | *["metadata", "graphql", "pgdump", "config"]
|
||||
|
||||
// HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS
|
||||
inferFunctionPermissions: bool | *true
|
||||
|
||||
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
|
||||
liveQueriesMultiplexedRefetchInterval: uint32 | *1000
|
||||
|
||||
// HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES
|
||||
stringifyNumericTypes: bool | *false
|
||||
}
|
||||
|
||||
authHook?: {
|
||||
// HASURA_GRAPHQL_AUTH_HOOK
|
||||
url: string
|
||||
|
||||
// HASURA_GRAPHQL_AUTH_HOOK_MODE
|
||||
mode: "GET" | *"POST"
|
||||
|
||||
// HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY
|
||||
sendRequestBody: bool | *true
|
||||
}
|
||||
|
||||
logs: {
|
||||
// HASURA_GRAPHQL_LOG_LEVEL
|
||||
level: "debug" | "info" | "error" | *"warn"
|
||||
}
|
||||
|
||||
events: {
|
||||
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
|
||||
httpPoolSize: uint32 & >=1 & <=100 | *100
|
||||
}
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// APIs for hasura
|
||||
#HasuraAPIs: "metadata" | "graphql" | "pgdump" | "config"
|
||||
|
||||
// Configuration for storage service
|
||||
#Storage: {
|
||||
// Version of storage service, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-storage/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-storage/releases
|
||||
version: string | *"0.9.1"
|
||||
|
||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||
// configurations in the CDN. We will enable it again in the future.
|
||||
resources?: #Resources & {networking?: null}
|
||||
|
||||
antivirus?: {
|
||||
server: "tcp://run-clamav:3310"
|
||||
}
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// Configuration for functions service
|
||||
#Functions: {
|
||||
node: {
|
||||
version: 20 | *22
|
||||
}
|
||||
|
||||
resources?: {
|
||||
networking?: #Networking
|
||||
}
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// Configuration for postgres service
|
||||
#Postgres: {
|
||||
// Version of postgres, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/postgres/tags
|
||||
version: string | *"14.18-20250728-1"
|
||||
|
||||
// Resources for the service
|
||||
resources: {
|
||||
compute?: #ResourcesCompute
|
||||
storage: {
|
||||
capacity: uint32 & >=1 & <=1000 // GiB
|
||||
}
|
||||
|
||||
replicas?: 1
|
||||
|
||||
enablePublicAccess?: bool | *false
|
||||
}
|
||||
|
||||
settings?: {
|
||||
jit: "off" | "on" | *"on"
|
||||
maxConnections: int32 | *100
|
||||
sharedBuffers: string | *"128MB"
|
||||
effectiveCacheSize: string | *"4GB"
|
||||
maintenanceWorkMem: string | *"64MB"
|
||||
checkpointCompletionTarget: number | *0.9
|
||||
walBuffers: string | *"-1"
|
||||
defaultStatisticsTarget: int32 | *100
|
||||
randomPageCost: number | *4.0
|
||||
effectiveIOConcurrency: int32 | *1
|
||||
workMem: string | *"4MB"
|
||||
hugePages: string | *"try"
|
||||
minWalSize: string | *"80MB"
|
||||
maxWalSize: string | *"1GB"
|
||||
maxWorkerProcesses: int32 | *8
|
||||
maxParallelWorkersPerGather: int32 | *2
|
||||
maxParallelWorkers: int32 | *8
|
||||
maxParallelMaintenanceWorkers: int32 | *2
|
||||
walLevel: string | *"replica"
|
||||
maxWalSenders: int32 | *10
|
||||
maxReplicationSlots: int32 | *10
|
||||
archiveTimeout: int32 & >=300 & <=1073741823 | *300
|
||||
trackIoTiming: "on" | *"off"
|
||||
|
||||
// if pitr is on we need walLevel to set to replica or logical
|
||||
_validateWalLevelIsLogicalOrReplicaIfPitrIsEnabled: ( pitr == _|_ | walLevel == "replica" | walLevel == "logical") & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
pitr?: {
|
||||
retention: uint8 & 7
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration for auth service
|
||||
// You can find more information about the configuration here:
|
||||
// https://github.com/nhost/hasura-auth/blob/main/docs/environment-variables.md
|
||||
#Auth: {
|
||||
// Version of auth, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-auth/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-auth/releases
|
||||
version: string | *"0.43.0"
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
|
||||
elevatedPrivileges: {
|
||||
mode: "recommended" | "required" | *"disabled"
|
||||
}
|
||||
|
||||
redirections: {
|
||||
// AUTH_CLIENT_URL
|
||||
clientUrl: #Url | *"http://localhost:3000"
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS
|
||||
allowedUrls: [...string]
|
||||
}
|
||||
|
||||
signUp: {
|
||||
// Inverse of AUTH_DISABLE_SIGNUP
|
||||
enabled: bool | *true
|
||||
|
||||
// AUTH_DISABLE_NEW_USERS
|
||||
disableNewUsers: bool | *false
|
||||
|
||||
turnstile?: {
|
||||
secretKey: string
|
||||
}
|
||||
}
|
||||
|
||||
user: {
|
||||
roles: {
|
||||
// AUTH_USER_DEFAULT_ROLE
|
||||
default: #UserRole | *"user"
|
||||
// AUTH_USER_DEFAULT_ALLOWED_ROLES
|
||||
allowed: [...#UserRole] | *[default, "me"]
|
||||
}
|
||||
locale: {
|
||||
// AUTH_LOCALE_DEFAULT
|
||||
default: #Locale | *"en"
|
||||
// AUTH_LOCALE_ALLOWED_LOCALES
|
||||
allowed: [...#Locale] | *[default]
|
||||
}
|
||||
|
||||
gravatar: {
|
||||
// AUTH_GRAVATAR_ENABLED
|
||||
enabled: bool | *true
|
||||
// AUTH_GRAVATAR_DEFAULT
|
||||
default: "404" | "mp" | "identicon" | "monsterid" | "wavatar" | "retro" | "robohash" | *"blank"
|
||||
// AUTH_GRAVATAR_RATING
|
||||
rating: "pg" | "r" | "x" | *"g"
|
||||
}
|
||||
email: {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAILS
|
||||
allowed: [...#Email]
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAILS
|
||||
blocked: [...#Email]
|
||||
|
||||
}
|
||||
emailDomains: {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS
|
||||
allowed: [...string & net.FQDN]
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS
|
||||
blocked: [...string & net.FQDN]
|
||||
}
|
||||
}
|
||||
|
||||
session: {
|
||||
accessToken: {
|
||||
// AUTH_ACCESS_TOKEN_EXPIRES_IN
|
||||
expiresIn: uint32 | *900
|
||||
// AUTH_JWT_CUSTOM_CLAIMS
|
||||
customClaims: [...{
|
||||
key: =~"[a-zA-Z_]{1,}[a-zA-Z0-9_]*"
|
||||
value: string
|
||||
default?: string
|
||||
}] | *[]
|
||||
}
|
||||
|
||||
refreshToken: {
|
||||
// AUTH_REFRESH_TOKEN_EXPIRES_IN
|
||||
expiresIn: uint32 | *2592000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
method: {
|
||||
anonymous: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
emailPasswordless: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
otp: {
|
||||
email: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
}
|
||||
|
||||
emailPassword: {
|
||||
// Disabling email+password sign in is not implmented yet
|
||||
// enabled: bool | *true
|
||||
hibpEnabled: bool | *false
|
||||
emailVerificationRequired: bool | *true
|
||||
passwordMinLength: uint8 & >=3 | *9
|
||||
}
|
||||
|
||||
smsPasswordless: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
oauth: {
|
||||
apple: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
keyId: string
|
||||
teamId: string
|
||||
privateKey: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
keyId?: string
|
||||
teamId?: string
|
||||
privateKey?: string
|
||||
}
|
||||
audience?: string
|
||||
scope?: [...string]
|
||||
}
|
||||
azuread: {
|
||||
#StandardOauthProvider
|
||||
tenant: string | *"common"
|
||||
}
|
||||
bitbucket: #StandardOauthProvider
|
||||
discord: #StandardOauthProviderWithScope
|
||||
entraid: {
|
||||
#StandardOauthProvider
|
||||
tenant: string | *"common"
|
||||
}
|
||||
facebook: #StandardOauthProviderWithScope
|
||||
github: #StandardOauthProviderWithScope
|
||||
gitlab: #StandardOauthProviderWithScope
|
||||
google: #StandardOauthProviderWithScope
|
||||
linkedin: #StandardOauthProviderWithScope
|
||||
spotify: #StandardOauthProviderWithScope
|
||||
strava: #StandardOauthProviderWithScope
|
||||
twitch: #StandardOauthProviderWithScope
|
||||
twitter: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
consumerKey: string
|
||||
consumerSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
consumerKey?: string
|
||||
consumerSecret?: string
|
||||
}
|
||||
}
|
||||
windowslive: #StandardOauthProviderWithScope
|
||||
workos: {
|
||||
#StandardOauthProvider
|
||||
connection?: string
|
||||
organization?: string
|
||||
}
|
||||
}
|
||||
|
||||
webauthn: {
|
||||
enabled: bool | *false
|
||||
relyingParty?: {
|
||||
id: string | *""
|
||||
name?: string
|
||||
origins?: [...#Url] | *[redirections.clientUrl]
|
||||
}
|
||||
attestation: {
|
||||
timeout: uint32 | *60000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totp: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
issuer: string
|
||||
}
|
||||
if !enabled {
|
||||
issuer?: string
|
||||
}
|
||||
}
|
||||
|
||||
misc: {
|
||||
concealErrors: bool | *false
|
||||
}
|
||||
|
||||
rateLimit: #AuthRateLimit
|
||||
}
|
||||
|
||||
#RateLimit: {
|
||||
limit: uint32
|
||||
interval: string & time.Duration
|
||||
}
|
||||
|
||||
#AuthRateLimit: {
|
||||
emails: #RateLimit | *{limit: 10, interval: "1h"}
|
||||
sms: #RateLimit | *{limit: 10, interval: "1h"}
|
||||
bruteForce: #RateLimit | *{limit: 10, interval: "5m"}
|
||||
signups: #RateLimit | *{limit: 10, interval: "5m"}
|
||||
global: #RateLimit | *{limit: 100, interval: "1m"}
|
||||
}
|
||||
|
||||
#StandardOauthProvider: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
}
|
||||
}
|
||||
|
||||
#StandardOauthProviderWithScope: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
}
|
||||
audience?: string
|
||||
scope?: [...string]
|
||||
}
|
||||
|
||||
#Provider: {
|
||||
smtp?: #Smtp
|
||||
sms?: #Sms
|
||||
}
|
||||
|
||||
#Smtp: {
|
||||
user: string
|
||||
password: string
|
||||
sender: string
|
||||
host: string & net.FQDN | net.IP
|
||||
port: #Port
|
||||
secure: bool
|
||||
method: "LOGIN" | "CRAM-MD5" | "PLAIN"
|
||||
}
|
||||
|
||||
#Sms: {
|
||||
provider: "twilio"
|
||||
accountSid: string
|
||||
authToken: string
|
||||
messagingServiceId: string
|
||||
}
|
||||
|
||||
#UserRole: string
|
||||
#Url: string
|
||||
#Port: uint16
|
||||
#Email: =~"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
|
||||
#Locale: string & strings.MinRunes(2) & strings.MaxRunes(2)
|
||||
|
||||
// See https://hasura.io/docs/latest/auth/authentication/jwt/
|
||||
#JWTSecret:
|
||||
({
|
||||
type: "HS384" | "HS512" | *"HS256"
|
||||
key: string
|
||||
} |
|
||||
{
|
||||
type: "RS256" | "RS384" | "RS512"
|
||||
key: string
|
||||
signingKey?: string
|
||||
kid?: string
|
||||
} |
|
||||
{
|
||||
jwk_url: #Url | *null
|
||||
}) &
|
||||
{
|
||||
claims_format?: "stringified_json" | *"json"
|
||||
audience?: string
|
||||
issuer?: string
|
||||
allowed_skew?: uint32
|
||||
header?: string
|
||||
} & {
|
||||
claims_map?: [...#ClaimMap]
|
||||
|
||||
} &
|
||||
({
|
||||
claims_namespace: string | *"https://hasura.io/jwt/claims"
|
||||
} |
|
||||
{
|
||||
claims_namespace_path: string
|
||||
} | *{})
|
||||
|
||||
#ClaimMap: {
|
||||
claim: string
|
||||
{
|
||||
value: string
|
||||
} | {
|
||||
path: string
|
||||
default?: string
|
||||
}
|
||||
} & {}
|
||||
|
||||
#SystemConfig: {
|
||||
auth: {
|
||||
email: {
|
||||
templates: {
|
||||
s3Key?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graphql: {
|
||||
// manually enable graphi on a per-service basis
|
||||
// by default it follows the plan
|
||||
featureAdvancedGraphql: bool | *false
|
||||
}
|
||||
|
||||
postgres: {
|
||||
enabled: bool | *true
|
||||
majorVersion: "14" | "15" | "16" | "17" | *"14"
|
||||
if enabled {
|
||||
database: string
|
||||
}
|
||||
if !enabled {
|
||||
database?: string
|
||||
}
|
||||
connectionString: {
|
||||
backup: string
|
||||
hasura: string
|
||||
auth: string
|
||||
storage: string
|
||||
}
|
||||
|
||||
disk?: {
|
||||
iops: uint32 | *3000
|
||||
tput: uint32 | *125
|
||||
}
|
||||
|
||||
encryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // 32 bytes hex-encoded key
|
||||
oldEncryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // for key rotation
|
||||
}
|
||||
|
||||
persistentVolumesEncrypted: bool | *false
|
||||
}
|
||||
|
||||
#AI: {
|
||||
version: string | *"0.8.0"
|
||||
resources: {
|
||||
compute: #ComputeResources
|
||||
}
|
||||
|
||||
openai: {
|
||||
organization?: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
autoEmbeddings: {
|
||||
synchPeriodMinutes: uint32 | *5
|
||||
}
|
||||
|
||||
webhookSecret: string
|
||||
}
|
||||
|
||||
#Observability: {
|
||||
grafana: #Grafana
|
||||
}
|
||||
|
||||
#Grafana: {
|
||||
adminPassword: string
|
||||
|
||||
smtp?: {
|
||||
host: string & net.FQDN | net.IP
|
||||
port: #Port
|
||||
sender: string
|
||||
user: string
|
||||
password: string
|
||||
}
|
||||
|
||||
alerting: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
contacts: {
|
||||
emails?: [...string]
|
||||
pagerduty?: [...{
|
||||
integrationKey: string
|
||||
severity: string
|
||||
class: string
|
||||
component: string
|
||||
group: string
|
||||
}]
|
||||
discord?: [...{
|
||||
url: string
|
||||
avatarUrl: string
|
||||
}]
|
||||
slack?: [...{
|
||||
recipient: string
|
||||
token: string
|
||||
username: string
|
||||
iconEmoji: string
|
||||
iconURL: string
|
||||
mentionUsers: [...string]
|
||||
mentionGroups: [...string]
|
||||
mentionChannel: string
|
||||
url: string
|
||||
endpointURL: string
|
||||
}]
|
||||
webhook?: [...{
|
||||
url: string
|
||||
httpMethod: string
|
||||
username: string
|
||||
password: string
|
||||
authorizationScheme: string
|
||||
authorizationCredentials: string
|
||||
maxAlerts: int
|
||||
}]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#RunServicePort: {
|
||||
port: #Port
|
||||
type: "http" | "grpc" | "tcp" | "udp"
|
||||
publish: bool | *false
|
||||
ingresses: [#Ingress] | *[]
|
||||
_publish_supported_only_over_http: (
|
||||
publish == false || type == "http" || type == "grpc" ) & true @cuegraph(skip)
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
#RunServiceName: =~"^[a-z]([-a-z0-9]*[a-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(30)
|
||||
|
||||
// Resource configuration for a service
|
||||
#ComputeResources: {
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
cpu: uint32 & >=62 & <=14000
|
||||
// MiB: 128MiB to 30GiB
|
||||
memory: uint32 & >=128 & <=28720
|
||||
|
||||
// validate memory steps of 128 MiB
|
||||
_validateMemorySteps128: (mod(memory, 128) == 0) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
#RunServiceResources: {
|
||||
compute: #ComputeResources
|
||||
|
||||
storage: [...{
|
||||
name: #RunServiceName // name of the volume, changing it will cause data loss
|
||||
capacity: uint32 & >=1 & <=1000 // GiB
|
||||
path: string
|
||||
}] | *[]
|
||||
_storage_name_must_be_unique: list.UniqueItems([for s in storage {s.name}]) & true @cuegraph(skip)
|
||||
_storage_path_must_be_unique: list.UniqueItems([for s in storage {s.path}]) & true @cuegraph(skip)
|
||||
|
||||
// Number of replicas for a service
|
||||
replicas: uint8 & <=10
|
||||
|
||||
autoscaler?: #Autoscaler
|
||||
|
||||
_validateReplicasMustBeSmallerThanMaxReplicas: (replicas <= autoscaler.maxReplicas) & true @cuegraph(skip)
|
||||
|
||||
_replcas_cant_be_greater_than_1_when_using_storage: (len(storage) == 0 | (len(storage) > 0 & replicas <= 1 && autoscaler == _|_)) & true @cuegraph(skip)
|
||||
|
||||
_validate_cpu_memory_ratio_must_be_1_for_2: (math.Abs(compute.memory-compute.cpu*2.048) <= 1.024) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
#RunServiceImage: {
|
||||
image: string
|
||||
// content of "auths", i.e., { "auths": $THIS }
|
||||
pullCredentials?: string
|
||||
}
|
||||
|
||||
#HealthCheck: {
|
||||
port: #Port
|
||||
initialDelaySeconds: int | *30
|
||||
probePeriodSeconds: int | *60
|
||||
}
|
||||
|
||||
#EnvironmentVariable: {
|
||||
// Name of the environment variable
|
||||
name: =~"(?i)^[a-z_]{1,}[a-z0-9_]*"
|
||||
// Value of the environment variable
|
||||
value: string
|
||||
}
|
||||
|
||||
#RunServiceConfig: {
|
||||
name: #RunServiceName
|
||||
image: #RunServiceImage
|
||||
command: [...string]
|
||||
environment: [...#EnvironmentVariable] | *[]
|
||||
ports?: [...#RunServicePort] | *[]
|
||||
resources: #RunServiceResources
|
||||
healthCheck?: #HealthCheck
|
||||
}
|
||||
40
cli/mcp/resources/resources.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
)
|
||||
|
||||
func Instructions() string {
|
||||
return "The following resources are available:\n\n" +
|
||||
fmt.Sprintf("- %s: %s\n", CloudResourceURI, CloudDescription) +
|
||||
fmt.Sprintf("- %s: %s\n", GraphqlManagementResourceURI, GraphqlManagementDescription) +
|
||||
fmt.Sprintf("- %s: %s\n", NhostTomlResourceURI, NhostTomlResourceDescription)
|
||||
}
|
||||
|
||||
func Register(cfg *config.Config, server *server.MCPServer) error {
|
||||
nt := NewNhostToml()
|
||||
nt.Register(server)
|
||||
|
||||
if cfg.Cloud != nil {
|
||||
ct := NewCloud(cfg)
|
||||
ct.Register(server)
|
||||
}
|
||||
|
||||
enableGraphlManagement := false
|
||||
for _, project := range cfg.Projects {
|
||||
if project.ManageMetadata {
|
||||
enableGraphlManagement = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if enableGraphlManagement {
|
||||
gmt := NewGraphqlManagement()
|
||||
gmt.Register(server)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,18 +2,11 @@ package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
//go:embed schema.graphql
|
||||
var schemaGraphql string
|
||||
|
||||
//go:embed schema-with-mutations.graphql
|
||||
var schemaGraphqlWithMutations string
|
||||
|
||||
type Tool struct {
|
||||
graphqlURL string
|
||||
withMutations bool
|
||||
@@ -33,7 +26,6 @@ func NewTool(
|
||||
}
|
||||
|
||||
func (t *Tool) Register(mcpServer *server.MCPServer) error {
|
||||
t.registerGetGraphqlSchema(mcpServer)
|
||||
t.registerGraphqlQuery(mcpServer)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetGraphqlSchemaName = "cloud-get-graphql-schema"
|
||||
//nolint:lll
|
||||
ToolGetGraphqlSchemaInstructions = `Get GraphQL schema for the Nhost Cloud allowing operations on projects and organizations. Retrieve the schema before using the tool to understand the available queries and mutations. Projects are equivalent to apps in the schema. IDs are typically uuids`
|
||||
)
|
||||
|
||||
func (t *Tool) registerGetGraphqlSchema(mcpServer *server.MCPServer) {
|
||||
schemaTool := mcp.NewTool(
|
||||
ToolGetGraphqlSchemaName,
|
||||
mcp.WithDescription(ToolGetGraphqlSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Cloud Platform",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(schemaTool, t.handleGetGraphqlSchema)
|
||||
}
|
||||
|
||||
func (t *Tool) handleGetGraphqlSchema(
|
||||
_ context.Context, _ mcp.CallToolRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
schema := schemaGraphql
|
||||
if t.withMutations {
|
||||
schema = schemaGraphqlWithMutations
|
||||
}
|
||||
|
||||
return mcp.NewToolResultStructured(schema, schema), nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
const (
|
||||
ToolGraphqlQueryName = "cloud-graphql-query"
|
||||
//nolint:lll
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against the Nhost Cloud to perform operations on projects and organizations. It also allows configuring projects hosted on Nhost Cloud. Make sure you got the schema before attempting to execute any query. If you get an error while performing a query refresh the schema in case something has changed or you did something wrong. If you get an error indicating mutations are not allowed the user may have disabled them in the server, don't retry and ask the user they need to pass --with-cloud-mutations when starting mcp-nhost to enable them. Projects are apps.`
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against the Nhost Cloud to perform operations on projects and organizations. It also allows configuring projects hosted on Nhost Cloud. Make sure you got the schema before attempting to execute any query. If you get an error while performing a query refresh the schema in case something has changed or you did something wrong. If you get an error indicating mutations are not allowed the user may have disabled them in the server, don't retry and ask the user they need to pass --with-cloud-mutations when starting nhost's mcp to enable them. Projects are apps.`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
@@ -25,7 +25,6 @@ type GraphqlQueryRequest struct {
|
||||
}
|
||||
|
||||
func (t *Tool) registerGraphqlQuery(mcpServer *server.MCPServer) {
|
||||
t.registerGetGraphqlSchema(mcpServer)
|
||||
queryTool := mcp.NewTool(
|
||||
ToolGraphqlQueryName,
|
||||
mcp.WithDescription(ToolGraphqlQueryInstructions),
|
||||
@@ -60,7 +59,7 @@ func (t *Tool) handleGraphqlQuery(
|
||||
|
||||
allowedMutations := []string{}
|
||||
if t.withMutations {
|
||||
allowedMutations = nil
|
||||
allowedMutations = []string{"*"}
|
||||
}
|
||||
|
||||
var resp graphql.Response[any]
|
||||
@@ -70,7 +69,7 @@ func (t *Tool) handleGraphqlQuery(
|
||||
args.Query,
|
||||
args.Variables,
|
||||
&resp,
|
||||
nil,
|
||||
[]string{"*"},
|
||||
allowedMutations,
|
||||
t.interceptors...,
|
||||
); err != nil {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolConfigServerSchemaName = "local-config-server-get-schema"
|
||||
//nolint:lll
|
||||
ToolConfigServerSchemaInstructions = `Get GraphQL schema for the local config server. This tool is useful when the user is developing a project and wants help changing the project's settings.`
|
||||
)
|
||||
|
||||
func (t *Tool) registerGetConfigServerSchema(mcpServer *server.MCPServer) {
|
||||
configServerSchemaTool := mcp.NewTool(
|
||||
ToolConfigServerSchemaName,
|
||||
mcp.WithDescription(ToolConfigServerSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Config Server",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithBoolean(
|
||||
"includeQueries",
|
||||
mcp.Description("include queries in the schema"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithBoolean(
|
||||
"includeMutations",
|
||||
mcp.Description("include mutations in the schema"),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
configServerSchemaTool,
|
||||
mcp.NewStructuredToolHandler(t.handleConfigGetServerSchema),
|
||||
)
|
||||
}
|
||||
|
||||
type ConfigServerGetSchemaRequest struct {
|
||||
IncludeQueries bool `json:"includeQueries"`
|
||||
IncludeMutations bool `json:"includeMutations"`
|
||||
}
|
||||
|
||||
func (t *Tool) handleConfigGetServerSchema(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args ConfigServerGetSchemaRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
var introspection graphql.ResponseIntrospection
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
t.configServerURL,
|
||||
graphql.IntrospectionQuery,
|
||||
nil,
|
||||
&introspection,
|
||||
nil,
|
||||
nil,
|
||||
t.interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query GraphQL schema", err), nil
|
||||
}
|
||||
|
||||
var allowQueries, allowMutations []graphql.Queries
|
||||
if !args.IncludeQueries {
|
||||
allowQueries = []graphql.Queries{}
|
||||
}
|
||||
|
||||
if !args.IncludeMutations {
|
||||
allowMutations = []graphql.Queries{}
|
||||
}
|
||||
|
||||
schema := graphql.ParseSchema(
|
||||
introspection,
|
||||
graphql.Filter{
|
||||
AllowQueries: allowQueries,
|
||||
AllowMutations: allowMutations,
|
||||
},
|
||||
)
|
||||
|
||||
return mcp.NewToolResultStructured(schema, schema), nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolConfigServerQueryName = "local-config-server-query"
|
||||
//nolint:lll
|
||||
ToolConfigServerQueryInstructions = `Execute a GraphQL query against the local config server. This tool is useful to query and perform configuration changes on the local development project. Before using this tool, make sure to get the schema using the local-config-server-schema tool. To perform configuration changes this endpoint is all you need but to apply them you need to run 'nhost up' again. Ask the user for input when you need information about settings, for instance if the user asks to enable some oauth2 method and you need the client id or secret.`
|
||||
)
|
||||
|
||||
type ConfigServerQueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]any `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerConfigServerQuery(mcpServer *server.MCPServer) {
|
||||
configServerQueryTool := mcp.NewTool(
|
||||
ToolConfigServerQueryName,
|
||||
mcp.WithDescription(ToolConfigServerQueryInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Config Server",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"query",
|
||||
mcp.Description("graphql query to perform"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
"variables",
|
||||
mcp.Description("variables to use in the query"),
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
configServerQueryTool,
|
||||
mcp.NewStructuredToolHandler(t.handleConfigServerQuery),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tool) handleConfigServerQuery(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args ConfigServerQueryRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
if args.Query == "" {
|
||||
return mcp.NewToolResultError("query is required"), nil
|
||||
}
|
||||
|
||||
var resp graphql.Response[any]
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
t.configServerURL,
|
||||
args.Query,
|
||||
args.Variables,
|
||||
&resp,
|
||||
nil,
|
||||
nil,
|
||||
t.interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query graphql endpoint", err), nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("error marshalling response", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultStructured(resp, string(b)), nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetGraphqlManagementSchemaName = "local-get-management-graphql-schema"
|
||||
ToolGetGraphqlManagementSchemaInstructions = `
|
||||
Get GraphQL's management schema for an Nhost development project running locally via the Nhost
|
||||
CLI. This tool is useful to properly understand how manage hasura metadata, migrations,
|
||||
permissions, remote schemas, etc.
|
||||
|
||||
Use it before attempting to use ` + ToolManageGraphqlName
|
||||
)
|
||||
|
||||
func (t *Tool) registerGetGraphqlManagementSchema(mcpServer *server.MCPServer) {
|
||||
schemaTool := mcp.NewTool(
|
||||
ToolGetGraphqlManagementSchemaName,
|
||||
mcp.WithDescription(ToolGetGraphqlManagementSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL's Management Schema for Nhost Development Project",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(schemaTool, t.handleGetGraphqlManagementSchema)
|
||||
}
|
||||
|
||||
func (t *Tool) handleGetGraphqlManagementSchema(
|
||||
_ context.Context, _ mcp.CallToolRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
return mcp.NewToolResultText(graphql.Schema), nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetGraphqlSchemaName = "local-get-graphql-schema"
|
||||
//nolint:lll
|
||||
ToolGetGraphqlSchemaInstructions = `Get GraphQL schema for an Nhost development project running locally via the Nhost CLI. This tool is useful when the user is developing a project and wants help generating code to interact with their project's Graphql schema.`
|
||||
)
|
||||
|
||||
type GetGraphqlSchemaRequest struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerGetGraphqlSchema(mcpServer *server.MCPServer) {
|
||||
schemaTool := mcp.NewTool(
|
||||
ToolGetGraphqlSchemaName,
|
||||
mcp.WithDescription(ToolGetGraphqlSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Development Project",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"role",
|
||||
mcp.Description(
|
||||
"role to use when executing queries. Default to user but make sure the user is aware",
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(schemaTool, mcp.NewStructuredToolHandler(t.handleGetGraphqlSchema))
|
||||
}
|
||||
|
||||
func (t *Tool) handleGetGraphqlSchema(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args GetGraphqlSchemaRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
if args.Role == "" {
|
||||
return mcp.NewToolResultError("role is required"), nil
|
||||
}
|
||||
|
||||
interceptors := append( //nolint:gocritic
|
||||
t.interceptors,
|
||||
auth.WithRole(args.Role),
|
||||
)
|
||||
|
||||
var introspection graphql.ResponseIntrospection
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
t.graphqlURL,
|
||||
graphql.IntrospectionQuery,
|
||||
nil,
|
||||
&introspection,
|
||||
nil,
|
||||
nil,
|
||||
interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query GraphQL schema", err), nil
|
||||
}
|
||||
|
||||
schema := graphql.ParseSchema(
|
||||
introspection,
|
||||
graphql.Filter{
|
||||
AllowQueries: nil,
|
||||
AllowMutations: nil,
|
||||
},
|
||||
)
|
||||
|
||||
return mcp.NewToolResultText(schema), nil
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
graphqlURL string
|
||||
configServerURL string
|
||||
interceptors []func(ctx context.Context, req *http.Request) error
|
||||
}
|
||||
|
||||
func NewTool(
|
||||
graphqlURL string,
|
||||
configServerURL string,
|
||||
interceptors ...func(ctx context.Context, req *http.Request) error,
|
||||
) *Tool {
|
||||
return &Tool{
|
||||
graphqlURL: graphqlURL,
|
||||
configServerURL: configServerURL,
|
||||
interceptors: interceptors,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tool) Register(mcpServer *server.MCPServer) error {
|
||||
t.registerGetGraphqlSchema(mcpServer)
|
||||
t.registerGraphqlQuery(mcpServer)
|
||||
t.registerGetConfigServerSchema(mcpServer)
|
||||
t.registerConfigServerQuery(mcpServer)
|
||||
t.registerGetGraphqlManagementSchema(mcpServer)
|
||||
t.registerManageGraphql(mcpServer)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGraphqlQueryName = "local-graphql-query"
|
||||
//nolint:lll
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against an Nhost development project running locally via the Nhost CLI. This tool is useful to test queries and mutations during development. If you run into issues executing queries, retrieve the schema using the local-get-graphql-schema tool in case the schema has changed.`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
type GraphqlQueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]any `json:"variables,omitempty"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerGraphqlQuery(mcpServer *server.MCPServer) {
|
||||
queryTool := mcp.NewTool(
|
||||
ToolGraphqlQueryName,
|
||||
mcp.WithDescription(ToolGraphqlQueryInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Development Project",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"query",
|
||||
mcp.Description("graphql query to perform"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithObject(
|
||||
"variables",
|
||||
mcp.Description("variables to use in the query"),
|
||||
mcp.AdditionalProperties(true),
|
||||
),
|
||||
mcp.WithString(
|
||||
"role",
|
||||
mcp.Description(
|
||||
"role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ", //nolint:lll
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(queryTool, mcp.NewStructuredToolHandler(t.handleGraphqlQuery))
|
||||
}
|
||||
|
||||
func (t *Tool) handleGraphqlQuery(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args GraphqlQueryRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
if args.Query == "" {
|
||||
return mcp.NewToolResultError("query is required"), nil
|
||||
}
|
||||
|
||||
if args.Role == "" {
|
||||
return mcp.NewToolResultError("role is required"), nil
|
||||
}
|
||||
|
||||
interceptors := append( //nolint:gocritic
|
||||
t.interceptors,
|
||||
auth.WithRole(args.Role),
|
||||
)
|
||||
|
||||
var resp graphql.Response[any]
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
t.graphqlURL,
|
||||
args.Query,
|
||||
args.Variables,
|
||||
&resp,
|
||||
nil,
|
||||
nil,
|
||||
interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query graphql endpoint", err), nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("error marshalling response", err), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultStructured(resp, string(b)), nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetGraphqlSchemaName = "project-get-graphql-schema"
|
||||
ToolGetGraphqlSchemaInstructions = `Get GraphQL schema for an Nhost project running in the Nhost Cloud.`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrInvalidRequestBody = errors.New("invalid request body")
|
||||
)
|
||||
|
||||
type GetGraphqlSchemaRequest struct {
|
||||
Role string `json:"role"`
|
||||
ProjectSubdomain string `json:"projectSubdomain"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerGetGraphqlSchemaTool(mcpServer *server.MCPServer, projects string) {
|
||||
schemaTool := mcp.NewTool(
|
||||
ToolGetGraphqlSchemaName,
|
||||
mcp.WithDescription(ToolGetGraphqlSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL Schema for Nhost Project running on Nhost Cloud",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"role",
|
||||
mcp.Description(
|
||||
"role to use when executing queries. Default to user but make sure the user is aware",
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
"projectSubdomain",
|
||||
mcp.Description(
|
||||
fmt.Sprintf(
|
||||
"Project to get the GraphQL schema for. Must be one of %s, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names", //nolint:lll
|
||||
projects,
|
||||
),
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
mcpServer.AddTool(schemaTool, mcp.NewStructuredToolHandler(t.handleGetGraphqlSchema))
|
||||
}
|
||||
|
||||
func (t *Tool) handleGetGraphqlSchema(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args GetGraphqlSchemaRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
if args.Role == "" {
|
||||
return mcp.NewToolResultError("role is required"), nil
|
||||
}
|
||||
|
||||
if args.ProjectSubdomain == "" {
|
||||
return mcp.NewToolResultError("projectSubdomain is required"), nil
|
||||
}
|
||||
|
||||
project, ok := t.projects[args.ProjectSubdomain]
|
||||
if !ok {
|
||||
return mcp.NewToolResultError("project not configured to be accessed by an LLM"), nil
|
||||
}
|
||||
|
||||
interceptors := []func(ctx context.Context, req *http.Request) error{
|
||||
project.authInterceptor,
|
||||
auth.WithRole(args.Role),
|
||||
}
|
||||
|
||||
var introspection graphql.ResponseIntrospection
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
project.graphqlURL,
|
||||
graphql.IntrospectionQuery,
|
||||
nil,
|
||||
&introspection,
|
||||
nil,
|
||||
nil,
|
||||
interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query GraphQL schema", err), nil
|
||||
}
|
||||
|
||||
schema := graphql.ParseSchema(
|
||||
introspection,
|
||||
graphql.Filter{
|
||||
AllowQueries: nil,
|
||||
AllowMutations: nil,
|
||||
},
|
||||
)
|
||||
|
||||
return mcp.NewToolResultStructured(schema, schema), nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package local
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,22 +10,21 @@ import (
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolManageGraphqlName = "local-manage-graphql"
|
||||
ToolManageGraphqlName = "manage-graphql"
|
||||
ToolManageGraphqlInstructions = `
|
||||
Query GraphQL's management endpoints on an Nhost development project running locally via
|
||||
the Nhost CLI. This tool is useful to manage hasura metadata, migrations, permissions,
|
||||
remote schemas, database migrations, etc. It also allows to interact with the underlying
|
||||
database directly.
|
||||
Query GraphQL's management endpoints on an Nhost project running. This tool is useful to
|
||||
manage hasura metadata, migrations, permissions, remote schemas, database migrations,
|
||||
etc. It also allows to interact with the underlying database directly.
|
||||
|
||||
* Do not forget to use the base url in the endpoint.
|
||||
* Before using this tool always describe in natural languate what you are about to do.
|
||||
|
||||
## Metadata changes
|
||||
|
||||
* When changing metadata always use the /apis/migrate endpoint
|
||||
* When changing metadata ALWAYS use the /apis/migrate endpoint
|
||||
* Always perform a bulk request to avoid
|
||||
having to perform multiple requests
|
||||
* The admin user always has full permissions to everything by default, no need to configure
|
||||
@@ -55,8 +54,9 @@ const (
|
||||
)
|
||||
|
||||
type ManageGraphqlRequest struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Body string `json:"body"`
|
||||
Body string `json:"body"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerManageGraphql(mcpServer *server.MCPServer) {
|
||||
@@ -73,10 +73,14 @@ func (t *Tool) registerManageGraphql(mcpServer *server.MCPServer) {
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"endpoint",
|
||||
mcp.Description(
|
||||
"The GraphQL management endpoint to query. Use https://local.hasura.local.nhost.run as base URL",
|
||||
),
|
||||
"subdomain",
|
||||
mcp.Description("Project to perform the GraphQL management operation against"),
|
||||
mcp.Enum(t.cfg.Projects.Subdomains()...),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
"path",
|
||||
mcp.Description("The path for the HTTP request"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
@@ -137,20 +141,37 @@ func genericQuery(
|
||||
func (t *Tool) handleManageGraphql(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args ManageGraphqlRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
if args.Endpoint == "" {
|
||||
return mcp.NewToolResultError("endpoint is required"), nil
|
||||
}
|
||||
|
||||
if args.Body == "" {
|
||||
return mcp.NewToolResultError("body is required"), nil
|
||||
}
|
||||
|
||||
if args.Subdomain == "" {
|
||||
return mcp.NewToolResultError("projectSubdomain is required"), nil
|
||||
}
|
||||
|
||||
project, err := t.cfg.Projects.Get(args.Subdomain)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to get project configuration", err), nil
|
||||
}
|
||||
|
||||
if !project.ManageMetadata {
|
||||
return mcp.NewToolResultError("project does not allow metadata management"), nil
|
||||
}
|
||||
|
||||
if project.AdminSecret == nil {
|
||||
return mcp.NewToolResultError("project does not have an admin secret configured"), nil
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Add("Content-Type", "application/json")
|
||||
headers.Add("Accept", "application/json")
|
||||
|
||||
interceptors := []func(ctx context.Context, req *http.Request) error{
|
||||
auth.WithAdminSecret(*project.AdminSecret),
|
||||
}
|
||||
|
||||
response, err := genericQuery(
|
||||
ctx, args.Endpoint, args.Body, http.MethodPost, headers, t.interceptors,
|
||||
ctx, project.GetHasuraURL()+args.Path, args.Body, http.MethodPost, headers, interceptors,
|
||||
)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to execute query", err), nil
|
||||
@@ -1,90 +1,25 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
subdomain string
|
||||
graphqlURL string
|
||||
authInterceptor func(ctx context.Context, req *http.Request) error
|
||||
allowQueries []string
|
||||
allowMutations []string
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
projects map[string]Project
|
||||
}
|
||||
|
||||
func allowedQueries(allowQueries []string) []string {
|
||||
if len(allowQueries) == 1 && allowQueries[0] == "*" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return allowQueries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewTool(
|
||||
projList []config.Project,
|
||||
) (*Tool, error) {
|
||||
projects := make(map[string]Project)
|
||||
|
||||
for _, proj := range projList {
|
||||
authURL := fmt.Sprintf("https://%s.auth.%s.nhost.run/v1", proj.Subdomain, proj.Region)
|
||||
graphqlURL := fmt.Sprintf("https://%s.graphql.%s.nhost.run/v1", proj.Subdomain, proj.Region)
|
||||
|
||||
var interceptor func(ctx context.Context, req *http.Request) error
|
||||
|
||||
switch {
|
||||
case proj.AdminSecret != nil && *proj.AdminSecret != "":
|
||||
interceptor = auth.WithAdminSecret(*proj.AdminSecret)
|
||||
case proj.PAT != nil && *proj.PAT != "":
|
||||
var err error
|
||||
|
||||
interceptor, err = auth.WithPAT(authURL, *proj.PAT)
|
||||
if err != nil {
|
||||
return nil,
|
||||
fmt.Errorf("failed to create PAT interceptor for %s: %w", proj.Subdomain, err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf( //nolint:err113
|
||||
"project %s does not have a valid auth mechanism", proj.Subdomain)
|
||||
}
|
||||
|
||||
projects[proj.Subdomain] = Project{
|
||||
subdomain: proj.Subdomain,
|
||||
graphqlURL: graphqlURL,
|
||||
authInterceptor: interceptor,
|
||||
allowQueries: allowedQueries(proj.AllowQueries),
|
||||
allowMutations: allowedQueries(proj.AllowMutations),
|
||||
}
|
||||
}
|
||||
|
||||
cfg *config.Config,
|
||||
) *Tool {
|
||||
return &Tool{
|
||||
projects: projects,
|
||||
}, nil
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tool) Register(mcpServer *server.MCPServer) error {
|
||||
projectNames := make([]string, 0, len(t.projects))
|
||||
for _, proj := range t.projects {
|
||||
projectNames = append(projectNames, proj.subdomain)
|
||||
}
|
||||
|
||||
slices.Sort(projectNames)
|
||||
|
||||
projectNamesStr := strings.Join(projectNames, ", ")
|
||||
|
||||
t.registerGetGraphqlSchemaTool(mcpServer, projectNamesStr)
|
||||
t.registerGraphqlQuery(mcpServer, projectNamesStr)
|
||||
t.registerGraphqlQuery(mcpServer)
|
||||
t.registerManageGraphql(mcpServer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package project
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -13,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGraphqlQueryName = "project-graphql-query"
|
||||
ToolGraphqlQueryName = "graphql-query"
|
||||
//nolint:lll
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against a Nhost project running in the Nhost Cloud. This tool is useful to query and mutate live data running on an online projec. If you run into issues executing queries, retrieve the schema using the project-get-graphql-schema tool in case the schema has changed. If you get an error indicating the query or mutation is not allowed the user may have disabled them in the server, don't retry and tell the user they need to enable them when starting mcp-nhost`
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against a Nhost project. This tool is useful to query and mutate data. If you run into issues executing queries, retrieve the schema again in case the schema has changed. If you get an error indicating the query or mutation is not allowed the user may have disabled them in the server, don't retry and tell the user they need to enable them when starting nhost's mcp`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
@@ -23,18 +22,18 @@ func ptr[T any](v T) *T {
|
||||
}
|
||||
|
||||
type GraphqlQueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]any `json:"variables,omitempty"`
|
||||
ProjectSubdomain string `json:"projectSubdomain"`
|
||||
Role string `json:"role"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
Query string `json:"query"`
|
||||
Variables map[string]any `json:"variables,omitempty"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Role string `json:"role"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Tool) registerGraphqlQuery(mcpServer *server.MCPServer, projects string) {
|
||||
func (t *Tool) registerGraphqlQuery(mcpServer *server.MCPServer) {
|
||||
allowedMutations := false
|
||||
|
||||
for _, proj := range t.projects {
|
||||
if proj.allowMutations == nil || len(proj.allowMutations) > 0 {
|
||||
for _, proj := range t.cfg.Projects {
|
||||
if proj.AllowMutations == nil || len(proj.AllowMutations) > 0 {
|
||||
allowedMutations = true
|
||||
break
|
||||
}
|
||||
@@ -62,19 +61,15 @@ func (t *Tool) registerGraphqlQuery(mcpServer *server.MCPServer, projects string
|
||||
mcp.Description("variables to use in the query"),
|
||||
),
|
||||
mcp.WithString(
|
||||
"projectSubdomain",
|
||||
mcp.Description(
|
||||
fmt.Sprintf(
|
||||
"Project to get the GraphQL schema for. Must be one of %s, otherwise you don't have access to it. You can use cloud-* tools to resolve subdomains and map them to names", //nolint:lll
|
||||
projects,
|
||||
),
|
||||
),
|
||||
"subdomain",
|
||||
mcp.Description("Project to perform the GraphQL query against"),
|
||||
mcp.Enum(t.cfg.Projects.Subdomains()...),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
"role",
|
||||
mcp.Description(
|
||||
"role to use when executing queries. Default to user but make sure the user is aware. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ", //nolint:lll
|
||||
"role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ", //nolint:lll
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
@@ -95,7 +90,7 @@ func (t *Tool) handleGraphqlQuery(
|
||||
return mcp.NewToolResultError("query is required"), nil
|
||||
}
|
||||
|
||||
if args.ProjectSubdomain == "" {
|
||||
if args.Subdomain == "" {
|
||||
return mcp.NewToolResultError("projectSubdomain is required"), nil
|
||||
}
|
||||
|
||||
@@ -103,15 +98,18 @@ func (t *Tool) handleGraphqlQuery(
|
||||
return mcp.NewToolResultError("role is required"), nil
|
||||
}
|
||||
|
||||
project, ok := t.projects[args.ProjectSubdomain]
|
||||
if !ok {
|
||||
return mcp.NewToolResultError(
|
||||
"this project is not configured to be accessed by an LLM",
|
||||
), nil
|
||||
project, err := t.cfg.Projects.Get(args.Subdomain)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to get project configuration", err), nil
|
||||
}
|
||||
|
||||
authInterceptor, err := project.GetAuthInterceptor()
|
||||
if err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to get auth interceptor", err), nil
|
||||
}
|
||||
|
||||
interceptors := []func(ctx context.Context, req *http.Request) error{
|
||||
project.authInterceptor,
|
||||
authInterceptor,
|
||||
auth.WithRole(args.Role),
|
||||
}
|
||||
|
||||
@@ -122,12 +120,12 @@ func (t *Tool) handleGraphqlQuery(
|
||||
var resp graphql.Response[any]
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
project.graphqlURL,
|
||||
project.GetGraphqlURL(),
|
||||
args.Query,
|
||||
args.Variables,
|
||||
&resp,
|
||||
project.allowQueries,
|
||||
project.allowMutations,
|
||||
project.AllowQueries,
|
||||
project.AllowMutations,
|
||||
interceptors...,
|
||||
); err != nil {
|
||||
return mcp.NewToolResultErrorFromErr("failed to query graphql endpoint", err), nil
|
||||
|
||||
94
cli/mcp/tools/schemas/project_schema.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetGraphqlSchemaName = "project-get-graphql-schema"
|
||||
ToolGetGraphqlSchemaInstructions = `Get GraphQL schema for an Nhost project running in the Nhost Cloud.`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrInvalidRequestBody = errors.New("invalid request body")
|
||||
)
|
||||
|
||||
type GetGraphqlSchemaRequest struct {
|
||||
Role string `json:"role"`
|
||||
ProjectSubdomain string `json:"projectSubdomain"`
|
||||
}
|
||||
|
||||
func toQueries(q []string) []graphql.Queries {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
queries := make([]graphql.Queries, len(q))
|
||||
for i, v := range q {
|
||||
queries[i] = graphql.Queries{
|
||||
Name: v,
|
||||
DisableNesting: false,
|
||||
}
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
func (t *Tool) handleProjectGraphqlSchema(
|
||||
ctx context.Context,
|
||||
role string,
|
||||
subdomain string,
|
||||
summary bool,
|
||||
queries, mutations []string,
|
||||
) (string, error) {
|
||||
project, err := t.cfg.Projects.Get(subdomain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get project by subdomain: %w", err)
|
||||
}
|
||||
|
||||
authInterceptor, err := project.GetAuthInterceptor()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get auth interceptor: %w", err)
|
||||
}
|
||||
|
||||
interceptors := []func(ctx context.Context, req *http.Request) error{
|
||||
authInterceptor,
|
||||
auth.WithRole(role),
|
||||
}
|
||||
|
||||
var introspection graphql.ResponseIntrospection
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
project.GetGraphqlURL(),
|
||||
graphql.IntrospectionQuery,
|
||||
nil,
|
||||
&introspection,
|
||||
[]string{"*"},
|
||||
nil,
|
||||
interceptors...,
|
||||
); err != nil {
|
||||
return "", fmt.Errorf("failed to query GraphQL schema: %w", err)
|
||||
}
|
||||
|
||||
var schema string
|
||||
if summary {
|
||||
schema = graphql.SummarizeSchema(introspection)
|
||||
} else {
|
||||
schema = graphql.ParseSchema(
|
||||
introspection,
|
||||
graphql.Filter{
|
||||
AllowQueries: toQueries(queries),
|
||||
AllowMutations: toQueries(mutations),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
103
cli/mcp/tools/schemas/schemas.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
)
|
||||
|
||||
const (
|
||||
ToolGetSchemaName = "get-schema"
|
||||
ToolGetSchemaInstructions = `
|
||||
Get GraphQL/API schemas to interact with various services. Use the "service" parameter to
|
||||
specify which schema you want. Supported services are:
|
||||
|
||||
- project: Get GraphQL schema for an Nhost project. The "subdomain"
|
||||
parameter is required to specify which project to get the schema for. The "role"
|
||||
parameter can be passed to specify the role to use when fetching the schema (defaults
|
||||
to admin).
|
||||
`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewTool(cfg *config.Config) *Tool {
|
||||
return &Tool{cfg: cfg}
|
||||
}
|
||||
|
||||
func (t *Tool) Register(mcpServer *server.MCPServer) {
|
||||
queryTool := mcp.NewTool(
|
||||
ToolGetSchemaName,
|
||||
mcp.WithDescription(ToolGetGraphqlSchemaInstructions),
|
||||
mcp.WithToolAnnotation(
|
||||
mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL/API schema for various services",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
),
|
||||
mcp.WithString(
|
||||
"role",
|
||||
mcp.Description(
|
||||
"role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ", //nolint:lll
|
||||
),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString(
|
||||
"subdomain",
|
||||
mcp.Description(
|
||||
"Project to get the GraphQL schema for. Required when service is `project`",
|
||||
),
|
||||
mcp.Enum(t.cfg.Projects.Subdomains()...),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithBoolean(
|
||||
"summary",
|
||||
mcp.Description("only return a summary of the schema"),
|
||||
mcp.DefaultBool(true),
|
||||
),
|
||||
mcp.WithArray(
|
||||
"queries",
|
||||
mcp.WithStringItems(),
|
||||
mcp.Description("list of queries to fetch"),
|
||||
),
|
||||
mcp.WithArray(
|
||||
"mutations",
|
||||
mcp.WithStringItems(),
|
||||
mcp.Description("list of mutations to fetch"),
|
||||
),
|
||||
)
|
||||
|
||||
mcpServer.AddTool(queryTool, mcp.NewStructuredToolHandler(t.handle))
|
||||
}
|
||||
|
||||
type HandleRequest struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Subdomain string `json:"subdomain,omitempty"`
|
||||
Summary bool `json:"summary,omitempty"`
|
||||
Queries []string `json:"queries,omitempty"`
|
||||
Mutations []string `json:"mutations,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Tool) handle(
|
||||
ctx context.Context, _ mcp.CallToolRequest, args HandleRequest,
|
||||
) (*mcp.CallToolResult, error) {
|
||||
schema, err := t.handleProjectGraphqlSchema(
|
||||
ctx, args.Role, args.Subdomain, args.Summary, args.Queries, args.Mutations,
|
||||
)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(schema), nil
|
||||
}
|
||||