Skip to content

feat: log each call under the agent whose token it carries - #10

Merged
fuseraft merged 2 commits into
masterfrom
feat/agent-tokens
Oct 18, 2026
Merged

fuseraft merged 2 commits into
masterfrom
feat/agent-tokens

Conversation

@fuseraft

@fuseraft fuseraft commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Until now the log said what ran and when, but not who ran it: every client shared one App:AuthToken.

  • Per-agent tokens. App:AgentTokens maps agent names to bearer tokens (App__AgentTokens__<name> as env vars) and replaces App:AuthToken. A blank token, or one shared by two agents, stops the server at startup. Revoking one agent no longer means rotating everyone's token.
  • Attribution. The middleware sets the matched agent as the request's user. The MCP tools read it from the request context and /api/query from HttpContext.User, and every log line names it. Over stdio, or over HTTP without tokens, it is unauthenticated.
  • Refusals are logged. Calls turned away before reaching the database (unknown database, rejected statement, invalid parameters, missing password) used to leave no trace. They're now warnings with the SQL, so the log shows what an agent tried, not only what ran.
  • list_databases is logged with the agent and the number of databases, and a describe_table that finds no table (or more than one) is logged as a failure instead of a run with 0 rows.
  • Rejected tokens are logged by SqlMcp.Http.HttpSecurity with method, path and remote address (never the token).
  • Timestamps. appsettings.Production.json starts each entry with the UTC time (journald keeps its own under systemd).
2026-10-18T14:02:40.019Z warn: SqlMcp.Services.QueryService[0]
      Refused query: DELETE FROM orders on prod for reporting: Only SELECT queries (including CTEs) are permitted.

Tested locally: all unit and HTTP tests, plus the Postgres integration tests. The other three databases are left to CI.

App:AgentTokens maps agent names to bearer tokens and replaces
App:AuthToken. The middleware sets the matched agent as the request's
user, and every query_database, list_tables, describe_table and
/api/query call is logged with its name ("unauthenticated" over stdio
or without tokens). Blank or shared tokens stop the server at startup.

Calls refused before they reach the database (unknown database, a
statement SqlGuard rejects, invalid parameters, a missing password) are
now logged as warnings with their SQL, and requests with a missing or
unknown token are logged with their path and remote address.

appsettings.Production.json starts each log entry with the UTC time.
…a failure

describe_table logged a missing or ambiguous table as having run, with
0 rows. The work passed to RunAsync now returns a result, so describe
reports that outcome as an error and it is logged as a failure.
list_databases, which logged nothing, now logs the agent and how many
databases it listed.
@fuseraft
fuseraft merged commit ef4f850 into master Oct 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant