Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
200 changes: 200 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
name: Runtime Integration Tests

on:
push:
branches: [ main, develop ]
paths:
- 'src/runtime/**'
- 'test/fixtures/**'
- '.github/workflows/integration-tests.yml'
pull_request:
branches: [ main ]
paths:
- 'src/runtime/**'
- 'test/fixtures/**'
- '.github/workflows/integration-tests.yml'

jobs:
test-runtime-behaviors:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fixture: [stateless-server, stateful-server]
node-version: [20, 22]
include:
- fixture: stateless-server
expected-schema-fields: 2
expected-required: 1
- fixture: stateful-server
expected-schema-fields: 2
expected-required: 0

name: Test ${{ matrix.fixture }} (Node ${{ matrix.node-version }})

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install CLI dependencies
run: |
npm install
npm run build

- name: Install fixture dependencies
run: |
cd test/fixtures/${{ matrix.fixture }}
npm install

- name: Build test server
run: |
cd test/fixtures/${{ matrix.fixture }}
node ../../../dist/index.js build src/index.ts

- name: Start test server
run: |
cd test/fixtures/${{ matrix.fixture }}
# Start server in background
PORT=8081 node .smithery/index.cjs &
echo $! > server.pid
# Wait for server to be ready
sleep 3

- name: Test config schema endpoint
run: |
cd test/fixtures/${{ matrix.fixture }}

# Test that server is responding
curl -f http://localhost:8081/.well-known/mcp-config > schema.json

# Validate schema structure
FIELD_COUNT=$(cat schema.json | jq -r '.properties | keys | length')
REQUIRED_COUNT=$(cat schema.json | jq -r '.required | length // 0')

echo "Expected fields: ${{ matrix.expected-schema-fields }}, Got: $FIELD_COUNT"
echo "Expected required: ${{ matrix.expected-required }}, Got: $REQUIRED_COUNT"

if [ "$FIELD_COUNT" != "${{ matrix.expected-schema-fields }}" ]; then
echo "Field count mismatch"
cat schema.json | jq .
exit 1
fi

if [ "$REQUIRED_COUNT" != "${{ matrix.expected-required }}" ]; then
echo "Required field count mismatch"
cat schema.json | jq .
exit 1
fi

echo "Schema validation passed"
cat schema.json | jq .

- name: Test MCP stateless/stateful behavior
run: |
cd test/fixtures/${{ matrix.fixture }}

# Test MCP tool calls to verify stateless vs stateful behavior
echo "Testing MCP behavior..."

if [ "${{ matrix.fixture }}" = "stateless-server" ]; then
# Stateless: each call creates fresh instance, no session needed
FIRST_CALL=$(curl -s -X POST "http://localhost:8081/mcp?apiKey=test-key&timeout=30" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "increment", "arguments": {}}}')

SECOND_CALL=$(curl -s -X POST "http://localhost:8081/mcp?apiKey=test-key&timeout=30" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "increment", "arguments": {}}}')

elif [ "${{ matrix.fixture }}" = "stateful-server" ]; then
# Stateful: requires proper MCP handshake and session management

# 1. Initialize session
INIT_RESPONSE=$(curl -s -D headers.tmp -X POST "http://localhost:8081/mcp?sessionTimeout=3600&maxConnections=100" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}')

# 2. Extract session ID
SESSION_ID=$(grep -i "mcp-session-id:" headers.tmp | cut -d' ' -f2 | tr -d '\r')
echo "Session ID: $SESSION_ID"

if [ -z "$SESSION_ID" ]; then
echo "Failed to get session ID from initialize"
exit 1
fi

# 3. Send initialized notification
curl -s -X POST "http://localhost:8081/mcp?sessionTimeout=3600&maxConnections=100" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}' > /dev/null

# 4. Make first tool call
FIRST_CALL=$(curl -s -X POST "http://localhost:8081/mcp?sessionTimeout=3600&maxConnections=100" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "increment", "arguments": {}}}')

# 5. Make second tool call with same session
SECOND_CALL=$(curl -s -X POST "http://localhost:8081/mcp?sessionTimeout=3600&maxConnections=100" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "increment", "arguments": {}}}')
fi

echo "First call: $FIRST_CALL"
echo "Second call: $SECOND_CALL"

# Extract callCount from SSE responses
FIRST_COUNT=$(echo "$FIRST_CALL" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq -r '.callCount')
SECOND_COUNT=$(echo "$SECOND_CALL" | grep "^data:" | sed 's/^data: //' | jq -r '.result.content[0].text' | jq -r '.callCount')

if [ "${{ matrix.fixture }}" = "stateless-server" ]; then
# Stateless: both calls should return 1 (fresh instance each time)
if [ "$FIRST_COUNT" = "1" ] && [ "$SECOND_COUNT" = "1" ]; then
echo "Stateless behavior confirmed: each call returns 1"
else
echo "Stateless behavior failed: expected both calls to return 1, got $FIRST_COUNT and $SECOND_COUNT"
exit 1
fi
elif [ "${{ matrix.fixture }}" = "stateful-server" ]; then
# Stateful: should increment (1, then 2)
if [ "$FIRST_COUNT" = "1" ] && [ "$SECOND_COUNT" = "2" ]; then
echo "Stateful behavior confirmed: calls incremented from $FIRST_COUNT to $SECOND_COUNT"
else
echo "Stateful behavior failed: expected 1 then 2, got $FIRST_COUNT and $SECOND_COUNT"
exit 1
fi
fi

- name: Cleanup
if: always()
run: |
cd test/fixtures/${{ matrix.fixture }}
if [ -f server.pid ]; then
kill $(cat server.pid) 2>/dev/null || true
fi
# Kill any remaining processes on port 8081/8082
pkill -f "node .smithery/index.cjs" || true

- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-artifacts-${{ matrix.fixture }}-node${{ matrix.node-version }}
path: |
test/fixtures/${{ matrix.fixture }}/schema.json
test/fixtures/${{ matrix.fixture }}/startup.log
test/fixtures/${{ matrix.fixture }}/.smithery/
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ jobs:
- run: npm run build
- run: npm run check

integration-tests:
uses: ./.github/workflows/integration-tests.yml

publish:
runs-on: ubuntu-latest
needs: [test, build]
needs: [test, build, integration-tests]

permissions:
contents: read
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [1.4.1] - 2025-09-27

### Added
- Integration tests for stateful/stateless server behavior validation

### Fixed
- Fixed config schema not being passed into server bundle

## [1.4.0] - TBD

### Added
- OAuth support

## [1.3.0] - 2025-09-12

### Added
- Created shared `cleanupChildProcess` utility for consistent process cleanup across commands
- Added bun bundler support in addition to esbuild - detected automatically at runtime with optional override with `--tool` option (note: only when using bun runtime for esbuild bundle; node doesn't allow bun api)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@smithery/cli",
"version": "1.4.0",
"version": "1.4.1",
"type": "module",
"private": false,
"homepage": "https://smithery.ai/",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/run/local-playground-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export const createLocalPlaygroundRunner = async (
12,
)(
"Thanks for using Smithery Local Playground!",
)}\n${chalk.blue("⬆ One-click cloud deploy: https://smithery.ai/new")}
)}\n${chalk.blue("⬆ One-click cloud deploy: https://smithery.ai/new")}
)}\n\n`,
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/run/uplink-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export const createUplinkRunner = async (
12,
)(
"Thanks for using Smithery Uplink!",
)}\n${chalk.blue("⬆ One-click cloud deploy: https://smithery.ai/new")}\n\n`,
)}\n${chalk.blue("⬆ One-click cloud deploy: https://smithery.ai/new")}\n\n`,
)
}

Expand Down
105 changes: 0 additions & 105 deletions src/runtime/__tests__/stateful-server.test.ts

This file was deleted.

Loading