Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
1eb6270
Initial commit
bpartridge83 Feb 26, 2025
2f56649
Initial commit
bpartridge83 Feb 26, 2025
bb45af2
Merge pull request #1 from twilio-internal/init
ktalebian Feb 26, 2025
1ead704
lint and openapi setup
Feb 26, 2025
236b9cc
lint and openapi setup
Feb 26, 2025
40ad1ac
Merge pull request #2 from twilio-internal/setup
ktalebian Feb 26, 2025
79c9009
add openapi spec support
Feb 26, 2025
6916aa2
add openapi spec support
Feb 26, 2025
a44dfdf
Merge pull request #3 from twilio-internal/add-openapi-spec
ktalebian Feb 27, 2025
89bee62
auto create accountSid
Feb 27, 2025
de5ce9a
remove build
Feb 27, 2025
b1b3cbd
cleanup
Feb 27, 2025
ca9c0c1
remove aloha
Feb 27, 2025
49c58c2
Merge pull request #4 from twilio-internal/account-sid
ktalebian Feb 27, 2025
a59a9a6
use public openapi
Feb 27, 2025
64978f6
use public openapi
Feb 27, 2025
4c3dc2d
Merge pull request #5 from twilio-internal/use-public-openapi
bpartridge83 Feb 27, 2025
708504e
better logs support
Feb 27, 2025
ed78944
Merge pull request #7 from twilio-internal/better-args
ktalebian Feb 27, 2025
94d8551
add readme
Feb 27, 2025
0a5b881
Merge pull request #8 from twilio-internal/add-readme
ktalebian Mar 3, 2025
603b88a
ability to select a service
Mar 3, 2025
7a3a1ac
remove local dir
Mar 3, 2025
c9117d1
Merge pull request #9 from twilio-internal/add-services
ktalebian Mar 3, 2025
c1ea6d5
Moving auth away from args to keytar, first swing at init command
bpartridge83 Mar 3, 2025
64f7be2
Handling undefined/unsupported command
bpartridge83 Mar 3, 2025
734c5bb
fix typing
Mar 3, 2025
5d1fc46
Moving `main`
bpartridge83 Mar 4, 2025
47ae9f9
Restoring option to pass arguments for accountSid, apiKey, and apiSecret
bpartridge83 Mar 4, 2025
4816e15
Removing log
bpartridge83 Mar 4, 2025
6340ecf
Standardize command execution
bpartridge83 Mar 4, 2025
34fb608
Merge pull request #11 from twilio-internal/fix-typing
ktalebian Mar 5, 2025
bd0c649
Merge pull request #10 from twilio-internal/init-auth
bpartridge83 Mar 5, 2025
124eef9
update package.json
Mar 7, 2025
5d0a798
add buildkite
Mar 7, 2025
36e2e5d
lint
Mar 7, 2025
7e51e08
lint
Mar 7, 2025
c61868b
Merge pull request #14 from twilio-internal/update-build
ktalebian Mar 7, 2025
8f0b053
fix urlencoded
Mar 12, 2025
4be7849
fix urlencoded
Mar 12, 2025
9539251
Merge pull request #16 from twilio-internal/fix-urlencoded
ktalebian Mar 12, 2025
4ecdd61
use tags
Mar 13, 2025
14eb62e
Merge pull request #17 from twilio-internal/add-tags
ktalebian Mar 13, 2025
2b59458
remove logs
Mar 13, 2025
97d3d9e
remove logs
Mar 13, 2025
3c621e8
remove logs
Mar 13, 2025
7c3f31f
remove static
Mar 13, 2025
472c10f
Merge pull request #18 from twilio-internal/remove-logs
ktalebian Mar 14, 2025
be16cd6
update services parsing
vingiarrusso Mar 18, 2025
24d8165
handle multiple
vingiarrusso Mar 18, 2025
2a41dad
update readme
vingiarrusso Mar 18, 2025
2e6c19f
update readme
vingiarrusso Mar 19, 2025
148d460
one more
vingiarrusso Mar 19, 2025
b3b5187
Merge pull request #19 from twilio-internal/services
ktalebian Mar 19, 2025
5a7eefc
better arg passing
Mar 19, 2025
d729cf6
better arg passing
Mar 19, 2025
f157532
Merge pull request #20 from twilio-internal/better-args
ktalebian Mar 19, 2025
9336106
add tests
Mar 19, 2025
8ebd801
Merge pull request #21 from twilio-internal/add-tests
ktalebian Mar 19, 2025
23e872d
add mcp-utils
Mar 21, 2025
a6bce67
add mcp-utils
Mar 21, 2025
6713764
add mcp-utils
Mar 21, 2025
74aa2cd
fix build
Mar 21, 2025
6e84282
lint
Mar 21, 2025
5afbcc4
Merge pull request #22 from twilio-internal/mcp-utils
ktalebian Mar 24, 2025
d451692
CLI `init` -> `config`
bpartridge83 Mar 25, 2025
3e39e34
Removing newlines
bpartridge83 Mar 25, 2025
33a8f67
More resturcture
bpartridge83 Mar 25, 2025
e604cba
Adding tests
bpartridge83 Mar 25, 2025
4f67f14
Fixing some lint errors
bpartridge83 Mar 25, 2025
fb6b215
Merge branch 'main' into cli-config-command
bpartridge83 Mar 25, 2025
80a56ee
Adding -y for npx exec
bpartridge83 Mar 25, 2025
74508ad
Removing buildkite
bpartridge83 Mar 25, 2025
c228cd6
Removing unnecessary files
bpartridge83 Mar 25, 2025
8cba768
Removing old init file
bpartridge83 Mar 25, 2025
fbd5445
Updating credential logic
bpartridge83 Mar 25, 2025
40a22cb
Linting fixes
bpartridge83 Mar 25, 2025
181409d
Updating PR validator to install keytar system dependencies
bpartridge83 Mar 26, 2025
3f36d76
Updating Workflow depedencies
bpartridge83 Mar 26, 2025
b399d9f
Merge branch 'main' into cli-config-command
bpartridge83 Mar 26, 2025
eba921a
Updating credentials settings
bpartridge83 Mar 27, 2025
d41b193
Merge remote-tracking branch 'origin/main' into cli-config-command
bpartridge83 Mar 27, 2025
6f32787
Updates lint issues
bpartridge83 Mar 28, 2025
c6e42c0
Merge branch 'main' into cli-config-command
bpartridge83 Mar 28, 2025
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
4 changes: 4 additions & 0 deletions .github/workflows/pr-validator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libsecret-1-dev
- uses: actions/setup-node@v4
with:
node-version: 20
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"packages/openapi-mcp-server",
"packages/mcp"
],
"bin": {
"twilio-mcp-server": "./packages/mcp/build/index.js"
},
"scripts": {
"build": "npm run build --workspaces",
"lint": "npm run lint --workspaces",
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ The easiest way to get started is to edit the configuration of your client to po
}
```

Alternatively, use the `npx -y @twilio-alpha config` command to set your credentials, choose tags and services, and automatically save MCP client configuration for Cursor or Claude Desktop.

Visit [Twilio API Keys docs](https://www.twilio.com/docs/iam/api-keys) for information on how to find/create your apiKey/apiSecret.

## Configuration Parameters
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@twilio-alpha/openapi-mcp-server": "0.1.2",
"@apidevtools/swagger-parser": "^10.1.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"chalk": "^5.4.1",
"inquirer": "^12.5.0",
"keytar": "^7.9.0",
"minimist": "^1.2.8",
Expand Down
316 changes: 316 additions & 0 deletions packages/mcp/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
/* eslint-disable no-console */
import fs from 'fs';
import os from 'os';
import path from 'path';

import chalk from 'chalk';
import inquirer from 'inquirer';

import { auth, isValidTwilioSid } from '@app/utils';

interface Credentials {
accountSid: string;
apiKey: string;
apiSecret: string;
}

interface MCPConfig {
mcpServers: {
[key: string]: {
command: string;
args: string[];
};
};
}

interface OpenAPIConfig {
tags?: string;
services?: string;
}

const CONFIG_PATHS = {
CURSOR: path.join(os.homedir(), '.cursor', 'mcp.json'),
CLAUDE: path.join(
os.homedir(),
'Library',
'Application Support',
'Claude',
'claude_desktop_config.json',
),
} as const;

const MCP_SERVER_NAME = 'twilio';
const DEFAULT_SERVICE = 'twilio_api_v2010';

async function readConfigFile(configPath: string): Promise<MCPConfig> {
try {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
}
} catch (error) {
console.error(
chalk.red('✗'),
`Error reading configuration file: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
return { mcpServers: {} };
}

async function configureCursor(executableArgs: string[]) {
console.info(chalk.green('👀'), 'Checking for Cursor configuration...');

const cursorConfig = await readConfigFile(CONFIG_PATHS.CURSOR);

if (MCP_SERVER_NAME in cursorConfig.mcpServers) {
console.info(
chalk.yellow('→'),
"Twilio MCP server already configured; we'll update it with the new configuration.",
);
}

cursorConfig.mcpServers[MCP_SERVER_NAME] = {
command: 'npx',
args: executableArgs,
};

try {
fs.writeFileSync(
CONFIG_PATHS.CURSOR,
JSON.stringify(cursorConfig, null, 2),
);
console.info(chalk.green('✔'), 'Cursor configuration set!');
} catch (error) {
console.error(
chalk.red('✗'),
`Failed to write Cursor configuration: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
process.exit(1);
}
}

async function configureClaudeDesktop(executableArgs: string[]) {
console.info(
chalk.green('👀'),
'Checking for Claude Desktop configuration...',
);

const existingConfig = await readConfigFile(CONFIG_PATHS.CLAUDE);

if (MCP_SERVER_NAME in existingConfig.mcpServers) {
console.info(
chalk.yellow('→'),
"Twilio MCP server already configured; we'll update it with the new configuration.",
);
}

existingConfig.mcpServers[MCP_SERVER_NAME] = {
command: 'npx',
args: executableArgs,
};

try {
fs.writeFileSync(
CONFIG_PATHS.CLAUDE,
JSON.stringify(existingConfig, null, 2),
);
console.info(chalk.green('✔'), 'Claude Desktop configuration set!');
} catch (error) {
console.error(
chalk.red('✗'),
`Failed to write Claude Desktop configuration: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
process.exit(1);
}
}

async function promptForOverwrite(currentSid: string): Promise<boolean> {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `Credentials already set for account \`${currentSid}\`. Overwrite?`,
default: false,
},
]);
return overwrite;
}

async function promptForCredentials(): Promise<Credentials> {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'accountSid',
message: 'Enter your Twilio account SID:',
validate: (input) =>
isValidTwilioSid(input, 'AC') || 'Invalid Account SID format',
},
{
type: 'password',
name: 'apiKey',
message: 'Enter your Twilio API Key SID:',
validate: (input) =>
isValidTwilioSid(input, 'SK') || 'Invalid API Key SID format',
},
{
type: 'password',
name: 'apiSecret',
message: 'Enter your Twilio API Key Secret:',
validate: (input) => input.length > 0 || 'API Secret is required',
},
]);

return answers;
}

async function promptForOpenAPIConfig(): Promise<OpenAPIConfig> {
const tagsAnswers = await inquirer.prompt([
{
type: 'confirm',
name: 'tags',
message: 'Do you want to use specific Twilio OpenAPI tags?',
default: false,
},
]);

if (tagsAnswers.tags) {
const { tags } = await inquirer.prompt([
{
type: 'input',
name: 'tags',
message:
'Enter the Twilio OpenAPI tags you want to use (comma-separated):',
validate: (input) => {
if (!input.trim()) {
return 'Tags cannot be empty';
}

const tagList = input.split(',').map((tag) => tag.trim());

return tagList.every((tag) => tag.length > 0) || 'Invalid tag format';
},
},
]);
return { tags };
}

const servicesAnswers = await inquirer.prompt([
{
type: 'confirm',
name: 'services',
message: 'Do you want to use specific Twilio OpenAPI services?',
default: false,
},
]);

if (servicesAnswers.services) {
const { services } = await inquirer.prompt([
{
type: 'input',
name: 'services',
message:
'Enter the Twilio OpenAPI services you want to use (comma-separated):',
validate: (input) => {
if (!input.trim()) {
return 'Services cannot be empty';
}

const serviceList = input.split(',').map((service) => service.trim());

return (
serviceList.every((service) => service.length > 0) ||
'Invalid service format'
);
},
},
]);
return { services };
}

console.info(
chalk.yellow('→'),
`No services selected; the default service will be used, \`${DEFAULT_SERVICE}\`.`,
);
return {};
}

async function configureClient(executableArgs: string[]) {
const { clientConfig } = await inquirer.prompt([
{
type: 'list',
name: 'clientConfig',
message: 'Which MCP client you want to configure?',
choices: [
{
name: 'None / Manual Configuration',
value: 'manual',
},
{
name: 'Cursor',
value: 'cursor',
},
{
name: 'Claude Desktop',
value: 'claude-desktop',
},
],
},
]);

switch (clientConfig) {
case 'cursor':
await configureCursor(executableArgs);
break;
case 'claude-desktop':
await configureClaudeDesktop(executableArgs);
break;
case 'manual':
default:
console.info(
chalk.green('👀'),
'Use the following `npx` command for the configuration in your MCP client:',
);
console.info(`npx ${executableArgs.join(' ')}`);
}
}

export default async function config() {
let currentCredentials = await auth.getCredentials();
let shouldOverwrite = false;

if (currentCredentials) {
shouldOverwrite = await promptForOverwrite(currentCredentials.accountSid);
}

if (currentCredentials && !shouldOverwrite) {
console.info(chalk.green('✔'), 'Keeping existing credentials');
} else {
const authAnswers = await promptForCredentials();
await auth.setCredentials(
authAnswers.accountSid,
authAnswers.apiKey,
authAnswers.apiSecret,
);
currentCredentials = await auth.getCredentials();
}

const openAPIConfig = await promptForOpenAPIConfig();

const executableArgs = ['-y', '@twilio-alpha/mcp'];

if (openAPIConfig.tags) {
executableArgs.push('--tags', openAPIConfig.tags);
}

if (openAPIConfig.services) {
executableArgs.push('--services', openAPIConfig.services);
}

await configureClient(executableArgs);
console.info(chalk.green('✔'), 'All set!');
}
8 changes: 4 additions & 4 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env node
import { logger } from '@twilio-alpha/openapi-mcp-server';

import init from '@app/init';
import config from '@app/config';
import main from '@app/main';

const command = process.argv[2];

if (command === 'init') {
init().catch((error) => {
logger.error(`Fatal error in init(): ${error}`);
if (command === 'config') {
config().catch((error) => {
logger.error(`Fatal error in config(): ${error}`);
process.exit(1);
});
} else {
Expand Down
Loading