-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Initial PostgreSQL MCP support #54431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
c657625
feat(mcp): initial postgres mcp
gabrielcorado c823e69
test(postgres): fix missing mock function
gabrielcorado 42d9402
fix(gomod): go mod tidy all
gabrielcorado a0d95ed
refactor: code review suggestions
gabrielcorado a5a24ee
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado 2623b70
fix(tsh): mcp init missing logger
gabrielcorado c3c4cc5
chore(tsh): missing other route to database field
gabrielcorado 1cfcf9c
refactor: use in-memory net listener
gabrielcorado f2dcd6e
test(tsh): add mcp db command test
gabrielcorado 10f7bf5
chore: fix license
gabrielcorado db60260
refactor(tsh): move logger init
gabrielcorado ab10408
test(mcp): sort slices to avoid flakiness
gabrielcorado ed95575
chore: fix lint
gabrielcorado ec53029
test(mcp): sort the resources before assertion
gabrielcorado 720fc0a
fix(mcp): update error handler for better message
gabrielcorado 8114969
refactor: code review suggestions
gabrielcorado 2ff9cf0
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado 770d79b
feat: add external error retriever for more accurate error messages
gabrielcorado 4044a39
refactor: use the same logger init for mcp purposes
gabrielcorado 0a42ae0
refactor: code review suggestions
gabrielcorado 1ab11e8
refactor(tsh): rename command to `tsh mcp db start`
gabrielcorado fdd113c
refactor(mcp): protect database resources with rw mutex
gabrielcorado 1416943
chore: update server godocs
gabrielcorado 64d76c2
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado f06c1bd
chore: go mod tidy
gabrielcorado 8ffb7d8
refactor: update command to take list of databases
gabrielcorado fa9e26b
chore(mcp): license
gabrielcorado afff0d1
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado c6237b3
chore(tsh): remove unused function
gabrielcorado 5c8924d
refactor: code review suggestions
gabrielcorado 4d3775b
refactor(tsh): validate duplicated databases in MCP configuration
gabrielcorado ac5716b
refactor(tsh): rename files to mcp_db
gabrielcorado 7888d75
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado 4662050
feat(mcp): add cluster name to the database resource
gabrielcorado fed7970
Merge branch 'master' into gabrielcorado/pg-mcp-init
gabrielcorado File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // Teleport | ||
| // Copyright (C) 2025 Gravitational, Inc. | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| package mcp | ||
|
|
||
| import ( | ||
| "errors" | ||
| "io" | ||
| "strings" | ||
|
|
||
| "github.com/gravitational/trace" | ||
|
|
||
| apiclient "github.com/gravitational/teleport/api/client" | ||
| "github.com/gravitational/teleport/lib/client/mcp" | ||
| ) | ||
|
|
||
| // ExtenralErrorRetriever returns an external error that might have happened. | ||
| // | ||
| // MCP servers don't have knowledge of other processes that might fail during | ||
| // their execution, such as authentication failures. This provider can be used | ||
| // to give them the necessary context to provide more accurate user messages. | ||
| type ExternalErrorRetriever interface { | ||
| // RetrieveError retrieves the error if any. | ||
| RetrieveError() error | ||
| } | ||
|
|
||
| // FormatErrorMessage formats the database MCP error messages. | ||
| // format. | ||
| func FormatErrorMessage(retreiver ExternalErrorRetriever, err error) error { | ||
| if retreiver != nil { | ||
| err = trace.NewAggregate(retreiver.RetrieveError(), err) | ||
| } | ||
|
|
||
| switch { | ||
| case errors.Is(err, apiclient.ErrClientCredentialsHaveExpired): | ||
| return trace.BadParameter(ReloginRequiredErrorMessage) | ||
| case strings.Contains(err.Error(), "connection reset by peer") || errors.Is(err, io.ErrClosedPipe): | ||
| return trace.BadParameter(LocalProxyConnectionErrorMessage) | ||
| } | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| const ( | ||
| // ReloginRequiredErrorMessage is the message returned to the MCP client | ||
| // when the tsh session expired. | ||
| ReloginRequiredErrorMessage = `It looks like your Teleport session expired, | ||
| you must relogin (using "tsh login" on a terminal) before continue using this | ||
| tool. After that, there is no need to update or relaunch the MCP client - just | ||
| try using it again.` | ||
| // LocalProxyConnectionErrorMessage is the message returned to the MCP client when | ||
| // the database client cannot connect to the local proxy. | ||
| LocalProxyConnectionErrorMessage = `Teleport MCP server is having issue while | ||
| establishing the database connection. You can verify the MCP logs for more | ||
| details on what is causing this issue. After identifying and fixing the issue | ||
| a restart on the MCP client might be necessary.` | ||
| // EmptyDatabasesListErrorMessage is the message returned to the MCP client when | ||
| // the started database server is serving no databases. | ||
| EmptyDatabasesListErrorMessage = `There are no active Teleport databases available | ||
| for use on the MCP server. You can check the MCP server logs to see if any | ||
| database was not included due to an error. You can also verify that the list | ||
| of databases on the MCP command is correct.` | ||
| ) | ||
|
|
||
| var ( | ||
| // WrongDatabaseURIFormatError is the message returned to the MCP client | ||
| // when it sends a malformed database resource URI. | ||
| WrongDatabaseURIFormatError = trace.BadParameter("Malformed database resource URI. Database resources must follow the format: %q", mcp.SampleDatabaseResource) | ||
| // DatabaseNotFoundError is the message returned to the MCP client when the | ||
| // requested database is not available as MCP resource. | ||
| DatabaseNotFoundError = trace.NotFound(`Database not found. Only registered databases | ||
| can be used. Ask the user to attach the database resource or list the available | ||
| resources with %q tool`, listDatabasesToolName) | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // Teleport | ||
| // Copyright (C) 2025 Gravitational, Inc. | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| package mcp | ||
|
|
||
| import ( | ||
| "context" | ||
| "log/slog" | ||
| "net" | ||
|
|
||
| "github.com/gravitational/teleport/api/types" | ||
| "github.com/gravitational/teleport/lib/client/mcp" | ||
| ) | ||
|
|
||
| // NewServerConfig configuration passed to the server constructors. | ||
| type NewServerConfig struct { | ||
| Logger *slog.Logger | ||
| RootServer *RootServer | ||
| Databases []*Database | ||
| } | ||
|
|
||
| // NewServerFunc the MCP server constructor function definition. | ||
| type NewServerFunc func(context.Context, *NewServerConfig) (Server, error) | ||
|
|
||
| // Server represents a MCP server. | ||
| type Server interface { | ||
| // Close closes the server. | ||
| Close(context.Context) error | ||
| } | ||
|
|
||
| // Registry represents the available databases MCP servers per protocol and | ||
| // their constructors. | ||
| type Registry map[string]NewServerFunc | ||
|
|
||
| // IsSupported returns if the database protocol is supported by any MCP server | ||
| // available. | ||
| func (m Registry) IsSupported(protocol string) bool { | ||
| _, ok := m[protocol] | ||
| return ok | ||
| } | ||
|
|
||
| // LookupFunc is the function used to resolve database address. Follows the | ||
| // net.Resolver.LookupAddr format. | ||
| type LookupFunc func(ctx context.Context, host string) (addrs []string, err error) | ||
|
|
||
| // DialContextFunc is a function used to dial the database. Follows the | ||
| // net.Dialer.DialContext format. | ||
| type DialContextFunc func(ctx context.Context, network string, addr string) (net.Conn, error) | ||
|
|
||
| // Database the database served by an MCP server. | ||
| type Database struct { | ||
| // DB contains all information from the database. | ||
| DB types.Database | ||
| // ClusterName is the cluster name where the database is located. | ||
| ClusterName string | ||
| // Addr is the address the MCP server used to create a new database | ||
| // connection. | ||
| Addr string | ||
| // DatabaseUser is the database username used on the connections. | ||
| DatabaseUser string | ||
| // DatabaseName is the database name used on the connections. | ||
| DatabaseName string | ||
| // ExternalErrorRetriever used to retrieve any external error that might | ||
| // have happened while connecting/communicating with the database. | ||
| ExternalErrorRetriever ExternalErrorRetriever | ||
| // LookupFunc is the lookup function to resolve database address. | ||
| LookupFunc LookupFunc | ||
| // DialContextFunc is the dial function used to connect to the database. | ||
| DialContextFunc DialContextFunc | ||
| } | ||
|
|
||
| // ResourceURI returns the database MCP resource URI. | ||
| func (d Database) ResourceURI() mcp.ResourceURI { | ||
| return mcp.NewDatabaseResourceURI(d.ClusterName, d.DB.GetName()) | ||
| } | ||
|
|
||
| // DatabaseResource MCP resource representation of a Teleport database. | ||
| type DatabaseResource struct { | ||
| types.Metadata | ||
| // URI is the MCP URI resource. | ||
| URI string `json:"uri"` | ||
| // Protocol is the database protocol. | ||
| Protocol string `json:"protocol"` | ||
| // ClusterName is the cluster the database is. | ||
| ClusterName string `json:"cluster_name"` | ||
| } | ||
|
|
||
| // ToolName generates a database access tool name. | ||
| func ToolName(protocol, name string) string { | ||
| return ToolPrefix + protocol + "_" + name | ||
| } | ||
|
|
||
| // ToolPrefix is the default tool prefix for every MCP tool. | ||
| const ToolPrefix = "teleport_" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| // Teleport | ||
| // Copyright (C) 2025 Gravitational, Inc. | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| package mcp | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "log/slog" | ||
| "sync" | ||
|
|
||
| "github.com/ghodss/yaml" | ||
| "github.com/gravitational/trace" | ||
| "github.com/mark3labs/mcp-go/mcp" | ||
| mcpserver "github.com/mark3labs/mcp-go/server" | ||
|
|
||
| "github.com/gravitational/teleport" | ||
| ) | ||
|
|
||
| // listDatabasesTool is the MCP tool that list all databases being served | ||
| // (from all protocols). | ||
| var listDatabasesTool = mcp.NewTool(listDatabasesToolName, | ||
| mcp.WithDescription("List database resources available to be used with Teleport tools."), | ||
| ) | ||
|
|
||
| // RootServer database access root MCP server. It includes common MCP tools and | ||
| // resources across different databases and serves as a root server where | ||
| // database-specific MCP servers register their tools. | ||
| type RootServer struct { | ||
| *mcpserver.MCPServer | ||
|
|
||
| mu sync.RWMutex | ||
| logger *slog.Logger | ||
| availableDatabases map[string]*Database | ||
| } | ||
|
|
||
| // NewRootServer initializes a new root MCP server. | ||
| func NewRootServer(logger *slog.Logger) *RootServer { | ||
| server := &RootServer{ | ||
| MCPServer: mcpserver.NewMCPServer(serverName, teleport.Version), | ||
| logger: logger, | ||
| availableDatabases: make(map[string]*Database), | ||
| } | ||
| server.AddTool(listDatabasesTool, server.ListDatabases) | ||
|
|
||
| return server | ||
| } | ||
|
|
||
| // ListDatabases tool function used to list all available/served databases. | ||
| func (s *RootServer) ListDatabases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||
| s.mu.RLock() | ||
| defer s.mu.RUnlock() | ||
|
|
||
| if len(s.availableDatabases) == 0 { | ||
| return mcp.NewToolResultError(EmptyDatabasesListErrorMessage), nil | ||
| } | ||
|
|
||
| var res []mcp.Content | ||
| for _, db := range s.availableDatabases { | ||
|
gabrielcorado marked this conversation as resolved.
|
||
| contents, err := encodeDatabaseResource(db) | ||
| if err != nil { | ||
| s.logger.ErrorContext(ctx, "error while list databases", "error", err) | ||
| return mcp.NewToolResultError(FormatErrorMessage(nil, err).Error()), nil | ||
| } | ||
| res = append(res, mcp.EmbeddedResource{Type: "resource", Resource: contents}) | ||
| } | ||
|
|
||
| return &mcp.CallToolResult{ | ||
| Content: res, | ||
| }, nil | ||
| } | ||
|
|
||
| // GetDatabaseResource resource handler for databases. | ||
| func (s *RootServer) GetDatabaseResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
| s.mu.RLock() | ||
| defer s.mu.RUnlock() | ||
|
|
||
| db, ok := s.availableDatabases[request.Params.URI] | ||
| if !ok { | ||
| return nil, trace.NotFound("Database is %q not available as MCP resource", request.Params.URI) | ||
| } | ||
|
|
||
| encodedDb, err := encodeDatabaseResource(db) | ||
| if err != nil { | ||
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| return []mcp.ResourceContents{encodedDb}, nil | ||
| } | ||
|
|
||
| // RegisterDatabase register a database on the root server. This make it | ||
| // available as a MCP resource. | ||
|
gabrielcorado marked this conversation as resolved.
|
||
| // | ||
| // TODO(gabrielcorado): support dynamically registering/deregistering databases | ||
| // after the server starts. | ||
| func (s *RootServer) RegisterDatabase(db *Database) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
|
|
||
| uri := db.ResourceURI().String() | ||
| s.availableDatabases[uri] = db | ||
|
gabrielcorado marked this conversation as resolved.
|
||
| s.AddResource(mcp.NewResource(uri, fmt.Sprintf("%s Datatabase", db.DB.GetName()), mcp.WithMIMEType(databaseResourceMIMEType)), s.GetDatabaseResource) | ||
| } | ||
|
|
||
| // ServeStdio starts serving the root MCP using STDIO transport. | ||
| func (s *RootServer) ServeStdio(ctx context.Context, in io.Reader, out io.Writer) error { | ||
| return trace.Wrap(mcpserver.NewStdioServer(s.MCPServer).Listen(ctx, in, out)) | ||
| } | ||
|
|
||
| func buildDatabaseResource(db *Database) DatabaseResource { | ||
| return DatabaseResource{ | ||
|
greedy52 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to include the database type (and possibly more metadata) here as well? E.g. to let LLM better differentiate between, say, self-hosted and RDS databases. Not a blocker though, I think we can keep tweaking this post-release. |
||
| Metadata: db.DB.GetMetadata(), | ||
| URI: db.ResourceURI().String(), | ||
| Protocol: db.DB.GetProtocol(), | ||
| ClusterName: db.ClusterName, | ||
| } | ||
| } | ||
|
|
||
| func encodeDatabaseResource(db *Database) (mcp.ResourceContents, error) { | ||
| resource := buildDatabaseResource(db) | ||
| out, err := yaml.Marshal(resource) | ||
| if err != nil { | ||
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| return mcp.TextResourceContents{ | ||
| URI: resource.URI, | ||
| MIMEType: databaseResourceMIMEType, | ||
| Text: string(out), | ||
| }, nil | ||
| } | ||
|
|
||
| const ( | ||
| // serverName is the database MCP server name. | ||
| serverName = "teleport_databases" | ||
| // listDatabasesTool is the list databases tool name. | ||
| listDatabasesToolName = ToolPrefix + "list_databases" | ||
| // databaseResourceMIMEType is the MIME type of the database resources. | ||
| databaseResourceMIMEType = "application/yaml" | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering - as we want to start exposing more resources and APIs over MCP, would it make sense to have a common root MCP server that exposes all tools/functions? And have all these sub-packages be able to register new tools with it. Right now this is specific to database access.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we had some discussions on this too but didn't come into any conclusion.
Cloudflare for example, does divide their servers by functionalities:
https://mcpservers.org/servers/cloudflare/mcp-server-cloudflare
personally i think all-in-one teleport mcp is very cluttered. but I may not necessarily separate them by resource type like db vs ssh. we could also do something similar to WebUI like "zero trust access" vs "identity security" (but could be cumbersome if some shared functionality).