Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/services/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,48 @@ following table describes the embedding configuration fields:
| `embedding_model` | string | The embedding model name (e.g., `voyage-3`, `text-embedding-3-small`, `nomic-embed-text`). Required when `embedding_provider` is set. |
| `embedding_api_key` | string | API key for the embedding provider. Required for `voyage` and `openai` providers. |

### Knowledgebase

Knowledgebase support enables the `search_knowledgebase` tool to query
a SQLite-backed knowledge base. The knowledge base file is staged on the
host; the Control Plane bind-mounts it into the container read-only.
Knowledgebase support is opt-in. When `kb_enabled` is `false` (the
default), no KB file is required — and any other `kb_*` fields present
in the config are **rejected** by the validator, not silently ignored.
Only `voyage` and `openai` are supported as embedding providers for the
knowledgebase; Ollama support is planned for a future release.

!!! warning

The Control Plane does not generate the knowledgebase SQLite file.
You must place the file on every host that will run an MCP service
instance **before** setting `kb_enabled: true`. If the file is
missing when the Control Plane attempts to deploy the service, the
deployment will be blocked with a clear error.

The default location is `{data_dir}/kb/nla-kb.db` (for example,
`/var/lib/pgedge-control-plane/kb/nla-kb.db`). Create the directory
and copy your file there, or use `kb_database_host_path` to specify
a custom path.

The following table describes the knowledgebase configuration fields:

| Field | Type | Description |
|---------------------------|---------|-------------|
| `kb_enabled` | boolean | Set to `true` to enable knowledgebase search. When `false` (the default), any other `kb_*` fields in the config are **rejected** — they must be removed before the config is accepted. |
| `kb_embedding_provider` | string | Embedding provider for the KB. One of: `voyage`, `openai`. Required when `kb_enabled` is `true`. |
| `kb_embedding_model` | string | Embedding model for the KB (e.g., `voyage-3-lite`, `text-embedding-3-small`). Required when `kb_enabled` is `true`. |
| `kb_embedding_api_key` | string | API key for the KB embedding provider. Required for `voyage` and `openai`. Scrubbed from API responses. |
| `kb_database_host_path` | string | Full path to the KB SQLite file on the host. Defaults to `{data_dir}/kb/nla-kb.db`. Must be an absolute path. |

!!! note

Changing any `kb_*` field (provider, model, credentials, or path)
requires a service redeploy — not just a config reload. SIGHUP only
reloads database connection settings and does not reinitialize the
knowledgebase. Use `update-database` to apply KB config changes; the
Control Plane will restart the container automatically.

### LLM Tuning

The LLM tuning fields control the behavior of the LLM proxy and are
Expand Down Expand Up @@ -333,6 +375,65 @@ to use a self-hosted Ollama server for both the LLM and embeddings:
}'
```

### Knowledgebase Search (Voyage AI)

In the following example, a `curl` command provisions an MCP service
with knowledgebase support enabled, using Voyage AI as the embedding
provider. Before provisioning, stage the knowledgebase file on every
host that will run an MCP service instance:

```sh
sudo mkdir -p /var/lib/pgedge-control-plane/kb
sudo cp /path/to/your/nla-kb.db /var/lib/pgedge-control-plane/kb/nla-kb.db
```

=== "curl"

```sh
curl -X POST http://host-1:3000/v1/databases \
-H 'Content-Type: application/json' \
--data '{
"id": "example",
"spec": {
"database_name": "example",
"nodes": [
{ "name": "n1", "host_ids": ["host-1"] }
],
"database_users": [
{
"username": "mcp_user",
"password": "changeme",
"db_owner": true,
"attributes": ["LOGIN"]
}
],
"services": [
{
"service_id": "mcp-server",
"service_type": "mcp",
"version": "latest",
"host_ids": ["host-1"],
"port": 8080,
"connect_as": "mcp_user",
"config": {
"kb_enabled": true,
"kb_embedding_provider": "voyage",
"kb_embedding_model": "voyage-3-lite",
"kb_embedding_api_key": "pa-..."
}
}
]
}
}'
```

To use a custom path for the knowledgebase file, add
`kb_database_host_path` to the `config` object:

```json
"kb_database_host_path": "/data/kb/my-kb.db"
```

## Connecting to the MCP Server

The MCP server accepts JSON-RPC 2.0 requests once the service instance
Expand Down
83 changes: 83 additions & 0 deletions server/internal/database/mcp_service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package database
import (
"encoding/json"
"fmt"
"path/filepath"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -52,6 +53,13 @@ type MCPServiceConfig struct {
DisableGenerateEmbedding *bool `json:"disable_generate_embedding,omitempty"`
DisableSearchKnowledgebase *bool `json:"disable_search_knowledgebase,omitempty"`
DisableCountRows *bool `json:"disable_count_rows,omitempty"`

// Optional - knowledgebase search
KBEnabled *bool `json:"kb_enabled,omitempty"`
KBEmbeddingProvider *string `json:"kb_embedding_provider,omitempty"`
KBEmbeddingModel *string `json:"kb_embedding_model,omitempty"`
KBEmbeddingAPIKey *string `json:"kb_embedding_api_key,omitempty"`
KBDatabaseHostPath *string `json:"kb_database_host_path,omitempty"`
}

// mcpKnownKeys is the set of all valid config keys for MCP service configuration.
Expand All @@ -78,10 +86,16 @@ var mcpKnownKeys = map[string]bool{
"disable_generate_embedding": true,
"disable_search_knowledgebase": true,
"disable_count_rows": true,
"kb_enabled": true,
"kb_embedding_provider": true,
"kb_embedding_model": true,
"kb_embedding_api_key": true,
"kb_database_host_path": true,
}

var validLLMProviders = []string{"anthropic", "openai", "ollama"}
var validEmbeddingProviders = []string{"voyage", "openai", "ollama"}
var validKBEmbeddingProviders = []string{"voyage", "openai"}

// ParseMCPServiceConfig parses and validates a config map into a typed MCPServiceConfig.
// If isUpdate is true, bootstrap-only fields (init_token, init_users) are rejected.
Expand Down Expand Up @@ -204,6 +218,24 @@ func ParseMCPServiceConfig(config map[string]any, isUpdate bool) (*MCPServiceCon
}
}

// Parse KB fields
kbEnabled, kbeErrs := optionalBool(config, "kb_enabled")
errs = append(errs, kbeErrs...)

isKBEnabled := kbEnabled != nil && *kbEnabled

kbEmbeddingProvider, kbepErrs := optionalString(config, "kb_embedding_provider")
errs = append(errs, kbepErrs...)

kbEmbeddingModel, kbemErrs := optionalString(config, "kb_embedding_model")
errs = append(errs, kbemErrs...)

kbEmbeddingAPIKey, kbeakErrs := optionalString(config, "kb_embedding_api_key")
errs = append(errs, kbeakErrs...)

kbDatabaseHostPath, kbdhpErrs := optionalString(config, "kb_database_host_path")
errs = append(errs, kbdhpErrs...)

// Parse optional fields
allowWrites, awErrs := optionalBool(config, "allow_writes")
errs = append(errs, awErrs...)
Expand Down Expand Up @@ -233,6 +265,52 @@ func ParseMCPServiceConfig(config map[string]any, isUpdate bool) (*MCPServiceCon
disableCountRows, dcrErrs := optionalBool(config, "disable_count_rows")
errs = append(errs, dcrErrs...)

// KB cross-validation: only when kb_enabled is true
if isKBEnabled {
// Conflict: kb_enabled + disable_search_knowledgebase is always broken
if disableSearchKB != nil && *disableSearchKB {
errs = append(errs, fmt.Errorf("kb_enabled and disable_search_knowledgebase cannot both be true: the search_knowledgebase tool would never register"))
}

if kbEmbeddingProvider == nil {
errs = append(errs, fmt.Errorf("kb_embedding_provider is required when kb_enabled is true"))
} else {
// ollama is not yet supported as a KB embedding provider
if strings.ToLower(*kbEmbeddingProvider) == "ollama" {
errs = append(errs, fmt.Errorf("kb_embedding_provider %q is not yet supported; use %q or %q", "ollama", "voyage", "openai"))
} else if !slices.Contains(validKBEmbeddingProviders, *kbEmbeddingProvider) {
errs = append(errs, fmt.Errorf("kb_embedding_provider must be one of: %s", strings.Join(validKBEmbeddingProviders, ", ")))
} else {
// voyage and openai require an API key
if kbEmbeddingAPIKey == nil {
errs = append(errs, fmt.Errorf("kb_embedding_api_key is required when kb_embedding_provider is %q", *kbEmbeddingProvider))
}
}
}

if kbEmbeddingModel == nil {
errs = append(errs, fmt.Errorf("kb_embedding_model is required when kb_enabled is true"))
}

// Path sanitization: must be absolute and clean (no .. components)
if kbDatabaseHostPath != nil {
p := *kbDatabaseHostPath
if !filepath.IsAbs(p) {
errs = append(errs, fmt.Errorf("kb_database_host_path must be an absolute path"))
} else if filepath.Clean(p) != p {
errs = append(errs, fmt.Errorf("kb_database_host_path must be a clean absolute path (no .. or redundant separators)"))
}
}
} else {
// KB is disabled — reject KB-specific fields to prevent silent misconfiguration
kbOnlyFields := []string{"kb_embedding_provider", "kb_embedding_model", "kb_embedding_api_key", "kb_database_host_path"}
for _, key := range kbOnlyFields {
if _, ok := config[key]; ok {
errs = append(errs, fmt.Errorf("%s must not be set unless kb_enabled is true", key))
}
}
}

if poolMaxConns != nil {
if *poolMaxConns <= 0 {
errs = append(errs, fmt.Errorf("pool_max_conns must be a positive integer"))
Expand Down Expand Up @@ -296,6 +374,11 @@ func ParseMCPServiceConfig(config map[string]any, isUpdate bool) (*MCPServiceCon
DisableGenerateEmbedding: disableGenEmbed,
DisableSearchKnowledgebase: disableSearchKB,
DisableCountRows: disableCountRows,
KBEnabled: kbEnabled,
KBEmbeddingProvider: kbEmbeddingProvider,
KBEmbeddingModel: kbEmbeddingModel,
KBEmbeddingAPIKey: kbEmbeddingAPIKey,
KBDatabaseHostPath: kbDatabaseHostPath,
}, nil
}

Expand Down
Loading