Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "eli0shin/mcp-controller" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
5 changes: 5 additions & 0 deletions .changeset/modern-foxes-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mcp-controller': minor
---

Add @total-typescript/ts-reset for improved type definitions and migrate to eslint-for-ai with type-safe JSON parsing. Also includes Changesets for automated release management.
28 changes: 28 additions & 0 deletions .claude/commands/changeset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
description: Create a changeset from git history
---

Create a changeset for the current branch automatically:

1. Gather all changes:
- Run `git log main..HEAD --oneline` to get all commits on this branch
- Run `git diff` to see unstaged changes
- Run `git diff --cached` to see staged changes
2. Analyze the commits and current changes to determine the semver bump:
- `major` if any commit contains "BREAKING" or "!"
- `minor` if any commit starts with "feat"
- `patch` otherwise (fix, chore, refactor, docs, etc.)
3. Generate a concise summary (1-2 sentences) describing all changes (commits + uncommitted)
4. Create a changeset file by running `bunx changeset add --empty` then editing the created file, OR by directly writing a new file to `.changeset/` with a random kebab-case name (e.g., `tall-lions.md`)

The changeset file format:

```markdown
---
'mcp-controller': <patch|minor|major>
---

<your generated summary>
```

Do not ask me any questions - determine everything from the git history and create the changeset file directly.
37 changes: 37 additions & 0 deletions .github/workflows/changeset-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Changeset Check

on:
pull_request:
branches:
- main

jobs:
check:
name: Check for changeset
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for source changes
id: changes
run: |
if git diff --name-only origin/main...HEAD | grep -q '^src/'; then
echo "src_changed=true" >> $GITHUB_OUTPUT
else
echo "src_changed=false" >> $GITHUB_OUTPUT
fi

- name: Setup Bun
if: steps.changes.outputs.src_changed == 'true'
uses: oven-sh/setup-bun@v2

- name: Install dependencies
if: steps.changes.outputs.src_changed == 'true'
run: bun install

- name: Check for changesets
if: steps.changes.outputs.src_changed == 'true'
run: bunx changeset status --since=origin/main --strict
41 changes: 41 additions & 0 deletions .github/workflows/version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Version

on:
push:
branches: [main]

jobs:
version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2

- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: bun install

- name: Build
run: bun run build

- name: Test
run: bun run test

- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: bunx changeset version
publish: bunx changeset publish
title: 'chore: version packages'
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
544 changes: 515 additions & 29 deletions bun.lock

Large diffs are not rendered by default.

55 changes: 9 additions & 46 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,53 +1,16 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import-x';
import globals from 'globals';
import forAi from 'eslint-for-ai';

export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
export default [
...forAi.configs.recommended,
{
languageOptions: {
globals: {
...globals.node,
settings: {
'import-x/resolver': {
typescript: true,
},
},
plugins: {
'import-x': importPlugin,
'import-x/core-modules': ['bun:test'],
},
rules: {
// TypeScript-specific rules
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],

// Import rules - enforce top-level ES module imports only
'import-x/no-commonjs': 'error',
'import-x/no-dynamic-require': 'error',
'import-x/no-amd': 'error',
'import-x/no-import-module-exports': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unused-vars': 'off',

// General rules
'no-console': 'error',
'prefer-const': 'error',

// Ban dynamic imports - only top-level import declarations allowed
'no-restricted-syntax': [
'error',
{
selector: 'ImportExpression',
message:
'Dynamic imports are not allowed. Use top-level import declarations only.',
},
],
'for-ai/no-standalone-class': 'off',
},
},
{
files: ['**/*.test.ts', 'tests/**/*'],
rules: {
'no-console': 'off',
},
}
);

];
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@
"prepublish": "bun test && bun run build"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.29.8",
"@modelcontextprotocol/sdk": "^1.0.0",
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^22.9.0",
"bun-types": "^1.1.34",
"eslint": "^9.15.0",
"eslint-plugin-import-x": "^4.4.2",
"globals": "^15.12.0",
"eslint-for-ai": "^1.0.8",
"prettier": "^3.3.3",
"typescript": "^5.6.3",
"typescript-eslint": "^8.14.0",
"zod": "^3.23.8"
},
"keywords": [
Expand Down
75 changes: 39 additions & 36 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bun

import { McpProxyServer } from './proxy-server.js';
import type { ProxyConfig, Tool } from './types.js';
import { parseJsonRpcResponse, parseToolsArray, type ProxyConfig } from './types.js';
import { TargetServerManager } from './target-server.js';
import { matchesToolPattern } from './utils.js';

Expand Down Expand Up @@ -166,7 +166,10 @@ async function listTools(config: ProxyConfig): Promise<void> {
const initResponse = initLines.find(line => line.trim());
if (!initResponse) throw new Error('No valid response received');

const parsedInitResponse = JSON.parse(initResponse);
const parsedInitResponse = parseJsonRpcResponse(JSON.parse(initResponse));
if (!parsedInitResponse) {
throw new Error('Invalid JSON-RPC response from server');
}
if (parsedInitResponse.error) {
throw new Error(`Initialize failed: ${parsedInitResponse.error.message}`);
}
Expand All @@ -187,39 +190,41 @@ async function listTools(config: ProxyConfig): Promise<void> {
const { value, done } = await reader.read();
if (done) break;

if (value) {
toolsBuffer += new TextDecoder().decode(value);
const lines = toolsBuffer.split('\n');
for (const line of lines) {
if (line.trim()) {
try {
const response = JSON.parse(line.trim());
if (response.id === 2) {
if (response.error) {
throw new Error(`Tools list failed: ${response.error.message}`);
}
// Apply filtering and display tools
let tools = response.result.tools || [];
if (config.enabledTools) {
tools = tools.filter((tool: Tool) => config.enabledTools!.some(pattern => matchesToolPattern(tool.name, pattern)));
} else if (config.disabledTools) {
tools = tools.filter((tool: Tool) => !config.disabledTools!.some(pattern => matchesToolPattern(tool.name, pattern)));
}
// Print tools in the requested format
for (const tool of tools) {
process.stdout.write(`${tool.name}: ${tool.description || 'No description available'}\n`);
}
return; // Exit successfully
toolsBuffer += new TextDecoder().decode(value);
const lines = toolsBuffer.split('\n');

for (const line of lines) {
if (line.trim()) {
try {
const response = parseJsonRpcResponse(JSON.parse(line.trim()));
if (response?.id === 2) {
if (response.error) {
throw new Error(`Tools list failed: ${response.error.message}`);
}

// Apply filtering and display tools
const rawTools = response.result?.tools;
let tools = parseToolsArray(rawTools);

const enabledTools = config.enabledTools;
const disabledTools = config.disabledTools;

if (enabledTools) {
tools = tools.filter((tool) => enabledTools.some(pattern => matchesToolPattern(tool.name, pattern)));
} else if (disabledTools) {
tools = tools.filter((tool) => !disabledTools.some(pattern => matchesToolPattern(tool.name, pattern)));
}

// Print tools in the requested format
for (const tool of tools) {
process.stdout.write(`${tool.name}: ${tool.description ?? 'No description available'}\n`);
}
} catch {
// Continue reading if this line wasn't valid JSON
continue;

return; // Exit successfully
}
} catch {
// Continue reading if this line wasn't valid JSON
continue;
}
}
}
Expand All @@ -231,9 +236,7 @@ async function listTools(config: ProxyConfig): Promise<void> {
process.stderr.write(`Error listing tools: ${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
} finally {
if (targetManager) {
await targetManager.stopTargetServer();
}
await targetManager.stopTargetServer();
}
}

Expand Down
Loading