Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
92 changes: 92 additions & 0 deletions docs/services/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,39 @@ 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a sentence in this section to tell people that they can get the knowledge base file from the pgEdge Postgres MCP release artifacts? It's unfortunate that the KB releases are separate from the normal tagged releases, so I think the best we can do is tell them to the download the kb.db file from the latest Knowledge Base release on the releases page: https://github.com/pgEdge/pgedge-postgres-mcp/releases.

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 no KB validation runs. For V1,
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), all other `kb_*` fields are ignored and the `search_knowledgebase` tool operates without a KB. |
| `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`. |

### LLM Tuning

The LLM tuning fields control the behavior of the LLM proxy and are
Expand Down Expand Up @@ -333,6 +366,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
64 changes: 64 additions & 0 deletions server/internal/database/mcp_service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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 +85,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 +217,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 +264,34 @@ 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 supported in V1
if *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"))
}
}

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 +355,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