<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vikrant Shete</title>
    <description>The latest articles on DEV Community by Vikrant Shete (@vikrant_shete_62ef8570825).</description>
    <link>https://dev.to/vikrant_shete_62ef8570825</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1665483%2Fd1e031fb-430c-4757-bd49-7d550588aa12.jpg</url>
      <title>DEV Community: Vikrant Shete</title>
      <link>https://dev.to/vikrant_shete_62ef8570825</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vikrant_shete_62ef8570825"/>
    <language>en</language>
    <item>
      <title>Speak to your database: Safely read your PG/SQL DB with Query Studio MCP server</title>
      <dc:creator>Vikrant Shete</dc:creator>
      <pubDate>Tue, 03 Nov 2026 10:22:31 +0000</pubDate>
      <link>https://dev.to/vikrant_shete_62ef8570825/speak-to-your-database-safely-read-your-pgsql-db-with-query-studio-mcp-server-2f0k</link>
      <guid>https://dev.to/vikrant_shete_62ef8570825/speak-to-your-database-safely-read-your-pgsql-db-with-query-studio-mcp-server-2f0k</guid>
      <description>&lt;p&gt;&lt;em&gt;
  How I built the query studio for natural language interactions with your
  database.&lt;/em&gt;
&lt;/p&gt;

&lt;p&gt;
  The inspiration for Query Studio came from a small SQL application I had
  already built. It could run a query, display a table, and export
  results. I wanted to make that workspace accessible through a
  conversation: ask a question in everyday language, see the SQL behind
  the answer, and follow up without starting over.
&lt;/p&gt;

&lt;p&gt;
  AI made it easier to explore the idea and iterate on the implementation.
  The decisions that interested me were how to give the assistant useful
  tools, control what those tools could execute, and make its answers
  inspectable. A database assistant becomes much more useful when the user
  can reliably ask the questions in natural langugage.
&lt;/p&gt;

&lt;p&gt;
  Query Studio connects to a PostgreSQL/SQL database and discovers its
  structure. It combines schema-aware SQL generation, validated read-only
  execution, a user query, and a result table in one workspace. This
  article follows those decisions, including the boundaries that still
  need care.
&lt;/p&gt;

&lt;h2&gt;The tools used by the MCP&lt;/h2&gt;

&lt;p&gt;
  I used MCP, the Model Context Protocol, to expose the application’s
  query capabilities through a defined interface. Each tool has an input
  contract, an output contract, and a specific job:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    construct_sql — Uses the database schema, the request, and retained
    context to generate SQL. It validates the completed statement and
    records the accepted query.
  &lt;/li&gt;
  &lt;li&gt;
    execute_query — Rechecks the SQL and runs exactly the accepted
    statement through the database layer. This tool makes no AI-model
    call.
  &lt;/li&gt;
  &lt;li&gt;
    summarize_results — Optionally sends the executed SQL and fetched rows
    to the AI model to produce an explanation. It runs after database
    execution.
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  The MCP client and server communicate inside the same application
  process, so this implementation does not require a separate MCP service
  to deploy. LangChain manages the agent’s decisions about the available
  tools. MCP defines how those capabilities are called, the application
  supplies their behavior and safeguards.
&lt;/p&gt;

&lt;p&gt;
  Each turn has an internal request ID linking its generated SQL,
  execution, and result. That keeps one turn’s work separate from
  another’s. The request state is released when the turn finishes, while
  the shared MCP connection remains available.
&lt;/p&gt;

&lt;h2&gt;How the MCP uses the tools&lt;/h2&gt;

&lt;p&gt;
  Before the assistant can write a useful query, the application needs to
  understand the database. It connects using the configured database
  connection and fetches schema information: accessible tables, columns,
  data types, and structural relationships. This metadata provides a map
  of what can be queried; discovering it does not require loading the
  tables’ business records.
&lt;/p&gt;

&lt;p&gt;
  The schema is cached to avoid repeating those discovery queries for
  every question. When the database structure changes, the schema
  explorer’s refresh action reloads the metadata. SQL generation can then
  use the updated structure.
&lt;/p&gt;

&lt;p&gt;
  Meaningful table and field names help the LLM connect a question to that
  structure. Names such as customer_id, outstanding_balance, and
  payment_due_date provide clues about the intended query.
  Natural-language descriptions attached to tables could add valuable
  business context too. The current implementation does not yet include
  those descriptions in the model context.
&lt;/p&gt;

&lt;p&gt;
  For a compatible PostgreSQL database, changing the connection
  configuration lets the application discover its schema without
  hand-writing a new table map for that dataset. The current driver,
  schema discovery, and SQL checks are PostgreSQL-specific. Supporting
  other SQL engines would require changes to those parts, and
  database-specific business definitions may still need guidance.
&lt;/p&gt;

&lt;p&gt;
  Consider the request ‘Show the top five customers by outstanding
  balance.’ The agent first decides whether fresh data is needed. If it
  is, construct_sql loads the cached schema, fetching it if necessary, and
  builds the SQL-generation context. A simplified example looks like this:
&lt;/p&gt;

&lt;pre&gt;System instructions:
Generate a single read-only PostgreSQL query using the supplied schema.

Retained conversation:
Relevant earlier questions, answers, SQL, and result context, if available.

Request context:
Discovered tables and columns, plus the current user request:
“Show the top five customers by outstanding balance.”&lt;/pre&gt;

&lt;p&gt;
  These are supplied as structured model messages. Instructions describe
  the task, while the schema and question supply the information to work
  with. That distinction helps explain how the assistant moves from a
  sentence to a query.
&lt;/p&gt;

&lt;p&gt;
  The accepted SQL then goes to execute_query. PostgreSQL returns the
  matching rows, and only afterward can the server invoke
  summarize_results when explanation is enabled. Generation and
  explanation are separate AI operations, with database execution between
  them. The summary tool is called by the server after execution rather
  than offered as another choice in the agent’s tool-selection loop.
&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnff3sevez06gbohdndd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnff3sevez06gbohdndd.png" alt="SQL construction, execution, and optional explanation happen in separate stages. Requests that need no fresh database data can receive a direct response." width="800" height="1168"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;SQL construction, execution, and optional explanation happen in separate stages. Requests that need no fresh database data can receive a direct response.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Making summarization an explicit choice&lt;/h2&gt;
&lt;p&gt;
  At the runner level, summarization is off by default when the
  summarizeResults option is omitted. It can be enabled by setting that
  configuration option to true. There is an important distinction in the
  currently deployed app: the config explicitly enables it.
&lt;/p&gt;

&lt;p&gt;
  Summarization sends fetched records to the model for explanation. For
  confidential data, I would recommend enabling it only with a private AI
  deployment approved to process those records, or another explicitly
  approved provider arrangement. A private deployment still needs
  appropriate access and data-handling controls.
&lt;/p&gt;

&lt;p&gt;
  Within the three tools, summarize_results is the one that directly makes
  a model call containing the newly executed result for explanation.
  execute_query performs the database read without calling a model.
  However, it would be inaccurate to say that turning summaries off keeps
  all fetched data away from the model: retained conversation history
  includes rows, and that history can be supplied to later agent and
  SQL-generation calls. A strict schema-only AI mode would also need to
  exclude rows from those context paths.
&lt;/p&gt;

&lt;p&gt;
  If an enabled summary fails after a successful query, the table remains
  useful. The application returns it with a warning instead of discarding
  the result.
&lt;/p&gt;

&lt;h2&gt;Generated SQL still has to be validated&lt;/h2&gt;

&lt;p&gt;
  A generated query is a proposal until it passes the application’s validation checks. Query Studio parses the completed SQL, accepts one SELECT-based
  statement, checks its referenced tables against the discovered schema,
  and rejects writes, deletes, schema changes, and other disallowed forms.
&lt;/p&gt;

&lt;p&gt;
  The validation also covers less obvious cases such as a write inside a common table expression (CTE), multiple statements in one request, SELECT
  INTO, and row-locking clauses. Checking whether the text begins with
  SELECT would leave too much unexamined.
&lt;/p&gt;

&lt;p&gt;
  Execution repeats the checks and requires the incoming SQL to match the
  statement recorded by construct_sql after normalization. The agent
  cannot generate one query and substitute a different one at the
  execution step.
&lt;/p&gt;

&lt;p&gt;
  The database should enforce its own boundary as well. For a private
  deployment, the connection should use a dedicated read-only database
  user, with SELECT access limited to the intended tables or views and no
  write or schema-management privileges. That role must be configured by
  the database administrator, the application does not automatically
  provision it.
&lt;/p&gt;

&lt;p&gt;
  Irrespective of the role, the app executes accepted queries inside a
  PostgreSQL read-only transaction. Application validation, a suitably
  restricted database user, and transaction-level enforcement provide
  distinct layers of protection. The first checks what the assistant
  proposes; the database controls what the connection is permitted to do.
&lt;/p&gt;

&lt;p&gt;
  Execution also has a ten-second statement timeout, a three-second lock
  timeout, and a maximum of 1,000 returned rows. An extra-row check
  detects truncation. ‘1,000 rows loaded’ describes the returned snapshot,
  not the total number of matches, and a row cap alone does not guarantee
  a cheap query.
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs62vfbzk845vm3xhtv7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs62vfbzk845vm3xhtv7h.png" alt="Application checks precede read-only database execution, with timeouts and a bounded result set. A restricted database role adds a deployment-level safeguard." width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Application checks precede read-only database execution, with timeouts and a bounded result set. A restricted database role adds a deployment-level safeguard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  These controls constrain execution. They cannot prove that the query
  answers the intended business question. A valid query can still use the
  wrong date range or count accounts when the user meant customers. The
  visible SQL gives the user something concrete to inspect. The public
  demonstration uses synthetic data and has no login; private deployments
  also need authentication and appropriate user-level access policies.
&lt;/p&gt;

&lt;h2&gt;Keeping the agent from calling indefinitely&lt;/h2&gt;

&lt;p&gt;
  An agent can keep reconsidering a task or repeatedly request tools
  without making progress. I used LangChain’s middleware mechanism to
  enforce a lifecycle around that behavior. The application’s custom
  middleware currently permits at most six model calls in the agent loop,
  then stops the turn with an error if another call is attempted.
&lt;/p&gt;

&lt;p&gt;
  The middleware also ends the agent loop once query execution has
  produced a result and stops further iteration after a tool failure.
  Repeated generation, execution, or summary operations within the same
  turn reuse their existing work instead of silently starting it again.
&lt;/p&gt;

&lt;p&gt;
  The six-call limit bounds the agent loop; SQL generation and optional
  summarization make additional model calls outside that count. This
  prevents an unbounded planning loop, but it does not eliminate
  hallucinations or guarantee a correct answer. Though, during the test runs across multiple scenarios no hallucinations were observed but depending on complexity of your database and the model of choice there is a finite possibility of inaccurate results. Query validation and
  result inspection address different parts of that problem.
&lt;/p&gt;

&lt;h2&gt;Beautifying the SQL&lt;/h2&gt;

&lt;p&gt;
  The SQL is a visible part of the answer. Users can inspect selected
  fields, filters, joins, and ordering instead of relying only on the
  explanation. CodeMirror provides syntax highlighting, and sql-formatter
  supplies PostgreSQL-aware indentation and line breaks for the display.
&lt;/p&gt;

&lt;p&gt;
  Streaming makes formatting less straightforward. A formatter needs
  enough meaningful SQL to parse a formattable block; an unfinished
  keyword or half-written expression may not be usable yet. A progressive
  formatter should therefore try only when a coherent block has arrived
  and keep the original text when formatting fails.
&lt;/p&gt;

&lt;p&gt;
  The current implementation takes the simpler approach: CodeMirror shows
  the incoming SQL while it streams, and sql-formatter runs after
  generation has completed successfully. Progressive block-by-block
  formatting would be a further refinement. Formatting affects the preview
  only, never the SQL sent for execution.
&lt;/p&gt;

&lt;p&gt;
  The table is part of that inspectable result too. Users can sort loaded
  rows, page through them, resize columns, and export a CSV of the
  snapshot. Those actions do not silently fetch additional data or rerun
  the query. The table implementation already existed in this project without any AI integration. Table was implemented using react-table to leverage the performance benefits and get out of the box sorting and other features.
&lt;/p&gt;

&lt;h2&gt;Supporting the multiple turns&lt;/h2&gt;

&lt;p&gt;
  After ‘Show the top five customers by balance,’ a follow-up such as
  ‘Explain that ranking’ can use retained context. ‘Only active customer,
  refresh the ranking’ or ‘show the home branch for these customers and find the top customers at those branches’ needs a new query. ‘Show the best ones’ may need
  clarification about what best means. A useful conversation makes room
  for all three responses.
&lt;/p&gt;

&lt;p&gt;
  The browser supplies retained conversation history to each fresh server
  turn. Currently the app server does not retain history. A saved pair includes the question and an answer context
  containing the explanation, SQL, metadata, and loaded rows. This lets a
  later request preserve earlier intent without treating the previous
  request ID as persistent memory.
&lt;/p&gt;

&lt;p&gt;
  History is limited to 40 messages, or up to 20 question-and-answer
  pairs, and 120,000 characters. The browser drops the oldest complete
  pairs when either budget is exceeded. It does not trim individual result
  rows to make a pair smaller. Even the newest pair can be removed if it
  cannot fit.
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31jcci3vqvfu6v8nsg4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31jcci3vqvfu6v8nsg4k.png" alt="A new turn receives bounded browser-held history and can explain an earlier result, ask for clarification, or request fresh data." width="800" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A new turn receives bounded browser-held history and can explain an earlier result, ask for clarification, or request fresh data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  Visible conversation and model context are separate. A table can remain
  on screen after its turn has left the assistant’s history. Retained
  results are also snapshots: a top-five result cannot establish a
  full-dataset total, and an earlier balance does not establish the
  current one. Fresh facts and unseen rows require a new query.
&lt;/p&gt;

&lt;h2&gt;Possible optimizations and improvements&lt;/h2&gt;

&lt;p&gt;
  One useful improvement would be adding natural-language table and column
  descriptions when that metadata is available. Names and types describe
  structure; descriptions can explain business meaning, preferred joins,
  units, or what a particular status represents. That would give the model
  more context than identifiers alone.
&lt;/p&gt;

&lt;p&gt;
  A second improvement is selective context trimming for longer
  conversations. A smaller language model could assess which earlier turns
  are relevant to the current question and retain a compact account of the
  necessary filters, definitions, and intent. That selection would need
  its own checks so it does not discard an important constraint or turn an
  uncertain detail into a fact. Any rows sent to that smaller model would
  need the same data-handling review.
&lt;/p&gt;

&lt;p&gt;
  Token-aware budgeting would complement the current message and character
  limits. I would also add evaluation questions with known results,
  especially for joins, date ranges, rankings, and follow-ups. Those
  checks would test whether queries capture the intended meaning, beyond
  being syntactically valid and permitted to run.
&lt;/p&gt;

&lt;h2&gt;Possible future extensions and applications&lt;/h2&gt;

&lt;p&gt;
  The same MCP structure could be extended into a Grafana dashboard
  assistant. Connect to the database, retrieve its schema and any useful
  description metadata, and let the model propose dashboard queries
  without sending business records during that design step. The user could
  review and select the queries, then an additional MCP tool could create
  or update a Grafana dashboard through its API.
&lt;/p&gt;

&lt;p&gt;
  That would need Grafana authentication, panel and data-source
  configuration, and a deliberate review step before posting changes. The
  current project does not implement that integration. Separating query
  proposals from dashboard publication would keep each responsibility
  clear.
&lt;/p&gt;

&lt;p&gt;
  Background triggers could make this workflow more proactive. For
  example, a schema refresh could detect a new column and suggest a
  dashboard panel. A database-side check could detect new field values or
  another custom condition and trigger a proposed data collection or
  report. A schema-only check can detect structural changes; detecting new
  values requires reading data, so that trigger’s permissions and model
  payload would need separate design.
&lt;/p&gt;

&lt;p&gt;
  Scheduled reporting is another possible extension: run approved
  read-only queries for observability, analytics, or regular operational
  summaries. A scheduler could collect the results, with an optional
  explanation when the chosen deployment is permitted to process them.
  Scheduling, delivery, and dashboard-writing tools would be additions to
  the current server.
&lt;/p&gt;

&lt;p&gt;
  These extensions build on the same foundation: discover the database,
  construct a query with context, validate it, and keep execution within
  explicit boundaries. Query Studio makes those stages visible enough for
  the user to inspect an answer and decide what to do next.
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://sql-query-app-lac.vercel.app/" rel="noopener noreferrer"&gt;See the live deployment here&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/vshete/sql-query-app" rel="noopener noreferrer"&gt;Explore the Query Studio project&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  For a deeper look at the architecture, MCP tool flow, query safeguards,
  and setup, read the
  &lt;a href="https://github.com/vshete/sql-query-app/blob/HEAD/docs/README.md" rel="noopener noreferrer"&gt;detailed Query Studio documentation on GitHub&lt;/a&gt;.
&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>database</category>
    </item>
  </channel>
</rss>
