add usage tracker
This commit is contained in:
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Builds
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
|
||||
# Vite / tooling caches
|
||||
.vite/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
bun-debug.log*
|
||||
|
||||
# Env files (keep .env.example committed)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# TypeScript incremental build info
|
||||
*.tsbuildinfo
|
||||
|
||||
# OS / editor cruft
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# JetBrains (either ignore all, or see "optional" note below)
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based HTTP Client
|
||||
http-client.private.env.json
|
||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# OpenAI MCP Server
|
||||
|
||||
An MCP server to fetch OpenAI usage data.
|
||||
|
||||
## Configuration
|
||||
|
||||
This server can authenticate in two ways:
|
||||
|
||||
### 1. Automated Authentication (Recommended)
|
||||
|
||||
Provide your OpenAI login credentials. The server will launch a headless browser to log in and retrieve session tokens.
|
||||
|
||||
- `OPENAI_EMAIL`: Your OpenAI account email.
|
||||
- `OPENAI_PASSWORD`: Your OpenAI account password.
|
||||
- `OPENAI_TOTP_SECRET`: Your 2FA secret (if 2FA is enabled).
|
||||
|
||||
### 2. Manual Authentication (Fallback)
|
||||
|
||||
Manually provide tokens if automated login fails.
|
||||
|
||||
- `OPENAI_AUTH_TOKEN`: The access token (from `https://chatgpt.com/api/auth/session`).
|
||||
- `OPENAI_COOKIE`: The full `Cookie` string from your browser session.
|
||||
|
||||
## Usage
|
||||
|
||||
### Automated Auth
|
||||
|
||||
```bash
|
||||
export OPENAI_EMAIL="your_email@example.com"
|
||||
export OPENAI_PASSWORD="your_password"
|
||||
export OPENAI_TOTP_SECRET="your_totp_secret"
|
||||
node build/index.js
|
||||
```
|
||||
|
||||
### Manual Auth
|
||||
|
||||
```bash
|
||||
export OPENAI_AUTH_TOKEN="your_token_here"
|
||||
export OPENAI_COOKIE="your_cookie_string_here"
|
||||
node build/index.js
|
||||
```
|
||||
|
||||
### integrating with MCP Clients
|
||||
|
||||
Add to your MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"openai": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/openai-mcp/build/index.js"],
|
||||
"env": {
|
||||
"OPENAI_AUTH_TOKEN": "your_token_here",
|
||||
"OPENAI_COOKIE": "your_cookie_string_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
- `get_usage`: Fetches the current usage statistics from OpenAI.
|
||||
3393
package-lock.json
generated
Normal file
3393
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "openai-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server for OpenAI usage integration",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"openai-mcp": "./build/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"start": "node build/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"openai",
|
||||
"usage"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.2",
|
||||
"axios": "^1.12.2",
|
||||
"otpauth": "^9.5.0",
|
||||
"puppeteer": "^24.37.2",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.9.1",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
482
src/index.ts
Normal file
482
src/index.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import puppeteer from "puppeteer-extra";
|
||||
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
||||
import { Page } from "puppeteer";
|
||||
|
||||
// Add stealth plugin to puppeteer
|
||||
const puppeteerExtra = puppeteer.default;
|
||||
puppeteerExtra.use(StealthPlugin());
|
||||
|
||||
import * as OTPAuth from "otpauth";
|
||||
|
||||
class AutomatedAuth {
|
||||
private email?: string;
|
||||
private password?: string;
|
||||
private totpSecret?: string;
|
||||
private browser: any;
|
||||
private page: Page | null = null;
|
||||
private sessionToken: string | null = null;
|
||||
private accessToken: string | null = null;
|
||||
|
||||
constructor(email?: string, password?: string, totpSecret?: string) {
|
||||
this.email = email;
|
||||
this.password = password;
|
||||
this.totpSecret = totpSecret;
|
||||
}
|
||||
|
||||
async loginAndGetToken(): Promise<{ accessToken: string; cookie: string }> {
|
||||
if (!this.email || !this.password) {
|
||||
throw new Error("Email and password are required for automated authentication.");
|
||||
}
|
||||
|
||||
console.error("Launching browser for authentication...");
|
||||
this.browser = await puppeteerExtra.launch({
|
||||
headless: true, // headless: "new" is deprecated, true is preferred now
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
try {
|
||||
this.page = await this.browser.newPage();
|
||||
await this.page!.setViewport({ width: 1280, height: 800 });
|
||||
|
||||
console.error("Navigating to login page...");
|
||||
await this.page!.goto("https://chatgpt.com/auth/login", {
|
||||
waitUntil: "networkidle2",
|
||||
});
|
||||
|
||||
// Click "Log in" button if present (sometimes it lands on a landing page)
|
||||
const loginButtonSelector = 'button[data-testid="login-button"]';
|
||||
if (await this.page!.$(loginButtonSelector)) {
|
||||
await this.page!.click(loginButtonSelector);
|
||||
await this.page!.waitForNavigation({ waitUntil: "networkidle0" });
|
||||
}
|
||||
|
||||
// Check for email input
|
||||
const emailInputSelector = 'input[name="email"], input[name="username"]'; // Generic selector attempt
|
||||
try {
|
||||
await this.page!.waitForSelector(emailInputSelector, { timeout: 10000 });
|
||||
} catch (e) {
|
||||
// Maybe already logged in or different flow?
|
||||
console.error("Could not find email input, checking if already logged in...");
|
||||
}
|
||||
|
||||
// Check if we are already on the password page (email filled and readonly)
|
||||
const emailReadonlySelector = 'input[name="username"][readonly]';
|
||||
if (await this.page!.$(emailReadonlySelector)) {
|
||||
console.error("Email already filled, proceeding to password...");
|
||||
} else {
|
||||
// Enter email
|
||||
console.error("Entering email...");
|
||||
await this.page!.type(emailInputSelector, this.email);
|
||||
|
||||
// Try to find the continue button more aggressively
|
||||
const continueBtnSelectors = [
|
||||
'button[value="validate"]',
|
||||
'button[name="intent"][value="validate"]',
|
||||
'button.continue-btn',
|
||||
'button[type="submit"]'
|
||||
];
|
||||
|
||||
let clicked = false;
|
||||
for (const selector of continueBtnSelectors) {
|
||||
try {
|
||||
const btn = await this.page!.$(selector);
|
||||
if (btn) {
|
||||
console.error(`Found continue button with selector: ${selector}`);
|
||||
await btn.click();
|
||||
clicked = true;
|
||||
// Wait for navigation or change in form
|
||||
try { await this.page!.waitForNavigation({ timeout: 5000 }); } catch (e) { }
|
||||
break;
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
console.error("Could not find a continue button, trying Enter key...");
|
||||
await this.page!.keyboard.press("Enter");
|
||||
try { await this.page!.waitForNavigation({ timeout: 5000 }); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for password input - based on HTML provided it might be 'input[name="current-password"]' or 'input[type="password"]'
|
||||
const passwordInputSelector = 'input[type="password"], input[name="current-password"], input[name="password"]';
|
||||
try {
|
||||
// Increase timeout to 20s
|
||||
await this.page!.waitForSelector(passwordInputSelector, { timeout: 20000 });
|
||||
} catch (e) {
|
||||
console.error("Password input not found.");
|
||||
|
||||
// Debug: Print page title and some body text
|
||||
const title = await this.page!.title();
|
||||
const content = await this.page!.evaluate(() => document.body.innerText.substring(0, 500));
|
||||
console.error(`Debug - Title: ${title}`);
|
||||
console.error(`Debug - Content Preview: ${content.replace(/\n/g, ' ')}`);
|
||||
|
||||
throw new Error(`Stuck on email page. Title: ${title}`);
|
||||
}
|
||||
|
||||
console.error("Entering password...");
|
||||
// Use a short delay before typing to ensure focusibility
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
await this.page!.type(passwordInputSelector, this.password);
|
||||
|
||||
// Click continue again after password - same button selector likely
|
||||
const submitBtnSelectors = [
|
||||
'button[value="validate"]',
|
||||
'button[name="intent"][value="validate"]',
|
||||
'button[type="submit"]'
|
||||
];
|
||||
|
||||
let clicked = false;
|
||||
for (const selector of submitBtnSelectors) {
|
||||
try {
|
||||
const btn = await this.page!.$(selector);
|
||||
if (btn) {
|
||||
console.error(`Found submit button with selector: ${selector}`);
|
||||
await btn.click();
|
||||
clicked = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
if (!clicked) {
|
||||
await this.page!.keyboard.press("Enter");
|
||||
}
|
||||
|
||||
console.error("Waiting for navigation or 2FA challenge...");
|
||||
// Wait a bit to see if we get redirected or if 2FA appears
|
||||
try {
|
||||
await this.page!.waitForNavigation({ waitUntil: "networkidle2", timeout: 5000 });
|
||||
} catch (e) {
|
||||
// Might be strictly waiting for 2FA input without nav
|
||||
}
|
||||
|
||||
// Check for 2FA input
|
||||
const totpInputSelector = 'input[type="text"][inputmode="numeric"]'; // Often 6 digit input
|
||||
// Or typically they might have specific classes/IDs.
|
||||
// Let's try to detect if we are on a 2FA page by looking for text "authentication app" or similar
|
||||
|
||||
let is2FA = false;
|
||||
try {
|
||||
if (await this.page!.$(totpInputSelector)) {
|
||||
is2FA = true;
|
||||
} else {
|
||||
// Check page content
|
||||
const content = await this.page!.content();
|
||||
if (content.includes("Enter code") || content.includes("authenticator app")) {
|
||||
is2FA = true;
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if (is2FA) {
|
||||
console.error("2FA challenge detected.");
|
||||
if (!this.totpSecret) {
|
||||
throw new Error("2FA is required but OPENAI_TOTP_SECRET is not set.");
|
||||
}
|
||||
|
||||
console.error("Generating TOTP code...");
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: OTPAuth.Secret.fromBase32(this.totpSecret),
|
||||
});
|
||||
const code = totp.generate();
|
||||
console.error(`Entering TOTP code: ${code}`);
|
||||
|
||||
await this.page!.waitForSelector(totpInputSelector);
|
||||
await this.page!.type(totpInputSelector, code);
|
||||
await this.page!.keyboard.press("Enter");
|
||||
|
||||
await this.page!.waitForNavigation({ waitUntil: "networkidle2", timeout: 30000 });
|
||||
}
|
||||
|
||||
console.error("Waiting for final navigation...");
|
||||
// Ensure we are fully logged in
|
||||
try {
|
||||
await this.page!.waitForNavigation({ waitUntil: "networkidle2", timeout: 5000 });
|
||||
} catch (e) { }
|
||||
|
||||
|
||||
// Check if we are logged in by looking for specific cookies or URL
|
||||
// access token is usually in the __Secure-next-auth.session-token cookie
|
||||
// but the API also uses Authorization: Bearer <accessToken>
|
||||
// The access token for the API is retrieved via /api/auth/session usually.
|
||||
|
||||
console.error("Waiting for session to stabilize...");
|
||||
// Let's try to grab the session from the API
|
||||
const sessionResponse = await this.page!.evaluate(async () => {
|
||||
try {
|
||||
const res = await fetch("https://chatgpt.com/api/auth/session");
|
||||
if (res.ok) {
|
||||
return await res.json();
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (sessionResponse && sessionResponse.accessToken) {
|
||||
this.accessToken = sessionResponse.accessToken;
|
||||
console.error("Successfully retrieved access token!");
|
||||
} else {
|
||||
console.error("Could not retrieve access token via fetch, trying cookies...");
|
||||
// Fallback to cookies
|
||||
}
|
||||
|
||||
const cookies = await this.page!.cookies();
|
||||
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
||||
|
||||
// If we didn't get access token from fetch, we might need to rely purely on cookies
|
||||
// Note: usage endpoint often requires the Bearer token which is the accessToken from the session object.
|
||||
|
||||
if (!this.accessToken) {
|
||||
// Try one more time to get session
|
||||
// Maybe we need to wait a bit more?
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
const sessionResponseRetry = await this.page!.evaluate(async () => {
|
||||
try {
|
||||
const res = await fetch("https://chatgpt.com/api/auth/session");
|
||||
if (res.ok) return await res.json();
|
||||
} catch (e) { return null; }
|
||||
});
|
||||
if (sessionResponseRetry?.accessToken) {
|
||||
this.accessToken = sessionResponseRetry.accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accessToken) {
|
||||
throw new Error("Failed to retrieve access token. Login might have failed or required captcha.");
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: this.accessToken,
|
||||
cookie: cookieString
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Authentication failed:", error);
|
||||
// Capture screenshot for debugging
|
||||
await this.page?.screenshot({ path: "auth-failure.png" });
|
||||
throw error;
|
||||
} finally {
|
||||
await this.browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenAiMcpServer {
|
||||
private server: Server;
|
||||
private axiosInstance: AxiosInstance | null = null;
|
||||
private automatedAuth: AutomatedAuth | null = null;
|
||||
private token: string | null = null;
|
||||
private cookie: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: "openai-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const email = process.env.OPENAI_EMAIL;
|
||||
const password = process.env.OPENAI_PASSWORD;
|
||||
const cookie = process.env.OPENAI_COOKIE;
|
||||
const authToken = process.env.OPENAI_AUTH_TOKEN;
|
||||
|
||||
const totpSecret = process.env.OPENAI_TOTP_SECRET;
|
||||
|
||||
if (email && password) {
|
||||
this.automatedAuth = new AutomatedAuth(email, password, totpSecret);
|
||||
} else if (cookie && authToken) {
|
||||
// Manual mode
|
||||
this.token = authToken;
|
||||
this.cookie = cookie;
|
||||
this.initAxios();
|
||||
} else {
|
||||
console.warn("Warning: No authentication credentials provided. Usage fetching will fail.");
|
||||
}
|
||||
|
||||
this.setupHandlers();
|
||||
this.setupErrorHandling();
|
||||
}
|
||||
|
||||
private initAxios() {
|
||||
if (!this.token || !this.cookie) return;
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: "https://chatgpt.com",
|
||||
headers: {
|
||||
"accept": "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"authorization": `Bearer ${this.token}`,
|
||||
"cookie": this.cookie,
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
// Add other headers as needed, but standard ones might suffice if cookies are correct
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private setupErrorHandling(): void {
|
||||
this.server.onerror = (error) => {
|
||||
console.error("[MCP Error]", error);
|
||||
};
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: this.getTools(),
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name } = request.params;
|
||||
try {
|
||||
switch (name) {
|
||||
case "get_usage":
|
||||
return await this.getUsage();
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${error.message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getTools(): Tool[] {
|
||||
return [
|
||||
{
|
||||
name: "get_usage",
|
||||
description: "Fetch OpenAI usage data. If credentials are unset, tries to log in automatically using OPENAI_EMAIL and OPENAI_PASSWORD.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async getUsage() {
|
||||
// If we don't have an axios instance (no manual creds), try to authenticate now
|
||||
if (!this.axiosInstance && this.automatedAuth) {
|
||||
try {
|
||||
const { accessToken, cookie } = await this.automatedAuth.loginAndGetToken();
|
||||
this.token = accessToken;
|
||||
this.cookie = cookie;
|
||||
this.initAxios();
|
||||
} catch (e: any) {
|
||||
throw new Error(`Automated authentication failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.axiosInstance) {
|
||||
throw new Error("Authentication failed or credentials missing. Please providing OPENAI_EMAIL/PASSWORD or OPENAI_AUTH_TOKEN/COOKIE.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance!.get("/backend-api/wham/usage");
|
||||
const data = response.data;
|
||||
|
||||
// Transform data to be more readable
|
||||
const formattedData = this.formatUsageData(data);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(formattedData, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
// If 401/403, and using automated auth, maybe we should retry login?
|
||||
// For now just error out.
|
||||
throw new Error(`Failed to fetch usage: ${error.message} - Status: ${error.response?.status} - Data: ${JSON.stringify(error.response?.data)}`);
|
||||
}
|
||||
throw new Error(`Failed to fetch usage: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private formatUsageData(data: any): any {
|
||||
if (!data) return data;
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
if (!seconds) return "0s";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
const parts = [];
|
||||
if (h > 0) parts.push(`${h}h`);
|
||||
if (m > 0) parts.push(`${m}m`);
|
||||
if (s > 0) parts.push(`${s}s`);
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
if (!timestamp) return null;
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const traverseAndFormat = (obj: any): any => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(traverseAndFormat);
|
||||
}
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
const newObj: any = {};
|
||||
for (const key in obj) {
|
||||
newObj[key] = traverseAndFormat(obj[key]);
|
||||
|
||||
// Add formatted fields
|
||||
if (key.endsWith('_at') && typeof obj[key] === 'number') {
|
||||
newObj[`${key}_formatted`] = formatDate(obj[key]);
|
||||
}
|
||||
if (key.endsWith('_seconds') && typeof obj[key] === 'number') {
|
||||
newObj[`${key}_readable`] = formatTime(obj[key]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
return traverseAndFormat(data);
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error("OpenAI MCP Server running on stdio");
|
||||
}
|
||||
}
|
||||
|
||||
const server = new OpenAiMcpServer();
|
||||
server.run().catch(console.error);
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user