Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b00480c
docs(mcp): design hosted MCP server
AchoArnold Sep 3, 2026
4c99540
docs(mcp): add implementation plan
AchoArnold Sep 3, 2026
8a5a4c8
feat(api): trust scoped MCP tokens
AchoArnold Sep 3, 2026
97d9786
feat(api): add incoming message endpoint
AchoArnold Sep 3, 2026
bf06243
feat(mcp): add service foundation
AchoArnold Sep 3, 2026
043f66f
fix(mcp): drop firebase SDK, make KeySet config one-shot
AchoArnold Sep 3, 2026
2e17e75
feat(mcp): add OAuth state and metadata
AchoArnold Sep 3, 2026
a748407
fix(mcp): harden OAuth metadata fetching
AchoArnold Sep 3, 2026
dfcf5e2
feat(mcp): add Firebase OAuth flow
AchoArnold Sep 3, 2026
e4ab70f
fix(mcp): harden OAuth authorization flow
AchoArnold Sep 3, 2026
505ecb2
feat(mcp): add httpSMS API client
AchoArnold Sep 3, 2026
5e4d7fc
fix(mcp): redact API query traces
AchoArnold Sep 3, 2026
86afa17
feat(mcp): add messaging tools
AchoArnold Sep 3, 2026
a13bc7d
feat(mcp): add API key tools
AchoArnold Sep 3, 2026
70617c2
fix(mcp): mark rotated keys sensitive
AchoArnold Sep 3, 2026
edec15f
feat(mcp): assemble hosted server
AchoArnold Sep 3, 2026
1786941
fix(mcp): harden server assembly
AchoArnold Sep 3, 2026
1740811
fix(mcp): rate limit rotation prompts
AchoArnold Sep 3, 2026
b980af0
chore(mcp): add Cloud Run deployment
AchoArnold Sep 4, 2026
f69a96f
fix(mcp): clarify deployment defaults
AchoArnold Sep 4, 2026
b95789a
test(mcp): add full integration suite
AchoArnold Sep 4, 2026
3bb6025
fix(tests): make MCP integration deterministic
AchoArnold Sep 4, 2026
ae19eb3
fix(tests): validate rate limit success path
AchoArnold Sep 4, 2026
318be45
ci(mcp): gate deploys on MCP tests
AchoArnold Sep 4, 2026
a44f222
Merge remote-tracking branch 'origin/main' into feat/mcp-server
AchoArnold Sep 4, 2026
3a2391c
fix(auth): bound token metadata caches
AchoArnold Sep 4, 2026
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
25 changes: 24 additions & 1 deletion .github/workflows/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,36 @@ jobs:
sleep 5
done

echo "Waiting for the MCP server to be healthy..."
for i in $(seq 1 40); do
if curl -sf http://localhost:8082/health >/dev/null 2>&1; then
echo "MCP server is healthy!"
break
fi
if [ $i -eq 40 ]; then
echo "MCP server failed to become healthy"
docker compose logs mcp
exit 1
fi
echo "MCP attempt $i/40 - waiting 5s..."
sleep 5
done

- name: Seed Database
working-directory: ./tests
run: |
echo "Waiting for seed container to finish..."
docker compose wait seed || true
sleep 2

- name: Run MCP Unit Tests
working-directory: ./mcp
run: go test -race -count=1 ./...

- name: Build MCP Server
working-directory: ./mcp
run: go build ./cmd/server

- name: Run Handler Integration Tests
working-directory: ./api
env:
Expand All @@ -85,7 +108,7 @@ jobs:

- name: Run Integration Tests
working-directory: ./tests
run: go test -v -timeout 300s ./...
run: go test -v -timeout 900s ./...

- name: Collect Logs on Failure
if: failure()
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ bash generate-firebase-credentials.sh
export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json)
docker compose up -d --build --wait
docker compose wait seed && sleep 2
go test -v -timeout 120s ./...
go test -v -timeout 900s ./...
docker compose down -v
```

Expand Down
101 changes: 101 additions & 0 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,107 @@ const docTemplate = `{
}
}
},
"/messages/incoming": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "This returns the list of mobile-originated messages received by the user's phones. This route is scoped to messages:read and never returns other message types",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Messages"
],
"summary": "Search incoming messages of a user",
"parameters": [
{
"type": "string",
"default": "+18005550199,+18005550100",
"description": "the owner's phone numbers",
"name": "owners",
"in": "query",
"required": true
},
{
"type": "string",
"description": "filter by message status",
"name": "statuses",
"in": "query"
},
{
"minimum": 0,
"type": "integer",
"description": "number of messages to skip",
"name": "skip",
"in": "query"
},
{
"type": "string",
"description": "filter messages containing query",
"name": "query",
"in": "query"
},
{
"type": "string",
"description": "field used to sort the messages",
"name": "sort_by",
"in": "query"
},
{
"type": "boolean",
"description": "sort messages in descending order",
"name": "sort_descending",
"in": "query"
},
{
"maximum": 200,
"minimum": 1,
"type": "integer",
"description": "number of messages to return",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/responses.MessagesResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.BadRequest"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/responses.Unauthorized"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/responses.UnprocessableEntity"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.InternalServerError"
}
}
}
}
},
"/messages/outstanding": {
"get": {
"security": [
Expand Down
101 changes: 101 additions & 0 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,107 @@
}
}
},
"/messages/incoming": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "This returns the list of mobile-originated messages received by the user's phones. This route is scoped to messages:read and never returns other message types",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Messages"
],
"summary": "Search incoming messages of a user",
"parameters": [
{
"type": "string",
"default": "+18005550199,+18005550100",
"description": "the owner's phone numbers",
"name": "owners",
"in": "query",
"required": true
},
{
"type": "string",
"description": "filter by message status",
"name": "statuses",
"in": "query"
},
{
"minimum": 0,
"type": "integer",
"description": "number of messages to skip",
"name": "skip",
"in": "query"
},
{
"type": "string",
"description": "filter messages containing query",
"name": "query",
"in": "query"
},
{
"type": "string",
"description": "field used to sort the messages",
"name": "sort_by",
"in": "query"
},
{
"type": "boolean",
"description": "sort messages in descending order",
"name": "sort_descending",
"in": "query"
},
{
"maximum": 200,
"minimum": 1,
"type": "integer",
"description": "number of messages to return",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/responses.MessagesResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.BadRequest"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/responses.Unauthorized"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/responses.UnprocessableEntity"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.InternalServerError"
}
}
}
}
},
"/messages/outstanding": {
"get": {
"security": [
Expand Down
69 changes: 69 additions & 0 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2920,6 +2920,75 @@ paths:
summary: Register a missed call event on the mobile phone
tags:
- Messages
/messages/incoming:
get:
consumes:
- application/json
description: This returns the list of mobile-originated messages received by
the user's phones. This route is scoped to messages:read and never returns
other message types
parameters:
- default: +18005550199,+18005550100
description: the owner's phone numbers
in: query
name: owners
required: true
type: string
- description: filter by message status
in: query
name: statuses
type: string
- description: number of messages to skip
in: query
minimum: 0
name: skip
type: integer
- description: filter messages containing query
in: query
name: query
type: string
- description: field used to sort the messages
in: query
name: sort_by
type: string
- description: sort messages in descending order
in: query
name: sort_descending
type: boolean
- description: number of messages to return
in: query
maximum: 200
minimum: 1
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/responses.MessagesResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.BadRequest'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/responses.Unauthorized'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/responses.UnprocessableEntity'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.InternalServerError'
security:
- ApiKeyAuth: []
summary: Search incoming messages of a user
tags:
- Messages
/messages/outstanding:
get:
consumes:
Expand Down
21 changes: 21 additions & 0 deletions api/pkg/auth/mcp_claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package auth

import "github.com/golang-jwt/jwt/v5"

// MCPClaims are the claims embedded in a delegated MCP API JWT minted by the
// hosted MCP service on behalf of an authenticated user. The token is scoped
// to a single API operation: it is only valid for the exact HTTP method and
// path it was minted for, and only when it carries the scope that operation
// requires.
type MCPClaims struct {
// Scopes are the downstream API scopes granted to this delegated token.
Scopes []string `json:"scopes"`

// Method is the HTTP method this delegated token is bound to.
Method string `json:"http_method"`

// Path is the HTTP request path this delegated token is bound to.
Path string `json:"http_path"`

jwt.RegisteredClaims
}
Loading
Loading