feat: add provider abstraction for review backends

This commit is contained in:
2026-06-12 19:23:45 +01:00
parent 1b8edb7eba
commit 732d6edd38
22 changed files with 1432 additions and 882 deletions
+1
View File
@@ -40,5 +40,6 @@ export function loadConfig() {
maxFiles: parseNum('CHATGPT_MCP_MAX_FILES', process.env.CHATGPT_MCP_MAX_FILES, 5),
maxLogChars: parseNum('CHATGPT_MCP_MAX_LOG_CHARS', process.env.CHATGPT_MCP_MAX_LOG_CHARS, 10000),
redactSecrets: parseBool('CHATGPT_MCP_REDACT_SECRETS', process.env.CHATGPT_MCP_REDACT_SECRETS, true),
chatgptMcpProvider: process.env.CHATGPT_MCP_PROVIDER || 'openai',
};
}
+33
View File
@@ -0,0 +1,33 @@
// Chat provider factory.
// Returns a chat provider based on config value CHATGPT_MCP_PROVIDER.
import { openaiProvider } from "./openai.js";
const SUPPORTED_PROVIDERS = new Set(["openai"]);
/**
* Create a chat provider from configuration.
* Defaults to OpenAI if no provider is specified or value is invalid.
* @param {object} config - Loaded config object (must contain openaiApiKey).
* @param {string} config.chatgptMcpProvider - Provider name (default: "openai").
* @returns {{ send: (input: object, cfg: object) => Promise<{ content: string }> }} Chat provider.
*/
export function createChatProvider(config) {
const providerName = config?.chatgptMcpProvider || "openai";
if (!SUPPORTED_PROVIDERS.has(providerName)) {
throw new Error(
`Unsupported chat provider "${providerName}". Supported providers: ${[...SUPPORTED_PROVIDERS].join(", ")}.`,
);
}
switch (providerName) {
case "openai":
return openaiProvider;
default:
// Should not reach here because of the set check above.
throw new Error(`Unknown provider "${providerName}".`);
}
}
export { openaiProvider };
+29
View File
@@ -0,0 +1,29 @@
// OpenAI chat provider adapter.
// Wraps src/openai/client.js + src/openai/responses.js into the provider interface.
import { createOpenAIClient } from "../openai/client.js";
import { sendOpenAIResponse } from "../openai/responses.js";
/**
* Chat provider backed by OpenAI Responses API.
* Implements { send(input, config) => Promise<{ content: string }> }.
*/
export const openaiProvider = {
/**
* @param {object} input - validated tool input (post-budget check).
* @param {object} config - full config from loadConfig().
* @returns {Promise<{ content: string }>}
*/
async send(input, config) {
const client = createOpenAIClient(config);
const result = await sendOpenAIResponse(client, {
input: [{ role: "system", content: input }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
return { content: result.content };
},
};
+26 -17
View File
@@ -7,8 +7,7 @@ import { handleReviewCode } from "./tools/review-code.js";
import { handleDebugIssue } from "./tools/debug-issue.js";
import { handleArchitectureReview } from "./tools/architecture-review.js";
import { loadConfig } from "./config/env.js";
import { createOpenAIClient } from "./openai/client.js";
import { sendOpenAIResponse } from "./openai/responses.js";
import { createChatProvider } from "./providers/factory.js";
const server = new McpServer({
name: "chatgpt-mcp",
@@ -23,10 +22,12 @@ server.registerTool(
inputSchema: baseInputSchema,
},
async (input) => {
const config = loadConfig();
const provider = createChatProvider(config);
const result = await handleAskChatGpt(input, {
loadConfig,
createOpenAIClient,
sendOpenAIResponse,
loadConfig: () => config,
createProvider: () => ({ send: provider.send.bind(provider) }),
});
const text = result.ok
@@ -51,10 +52,12 @@ server.registerTool(
inputSchema: baseInputSchema,
},
async (input) => {
const config = loadConfig();
const provider = createChatProvider(config);
const result = await handleReviewPlan(input, {
loadConfig,
createOpenAIClient,
sendOpenAIResponse,
loadConfig: () => config,
createProvider: () => ({ send: provider.send.bind(provider) }),
});
const text = result.ok
@@ -79,10 +82,12 @@ server.registerTool(
inputSchema: baseInputSchema,
},
async (input) => {
const config = loadConfig();
const provider = createChatProvider(config);
const result = await handleReviewCode(input, {
loadConfig,
createOpenAIClient,
sendOpenAIResponse,
loadConfig: () => config,
createProvider: () => ({ send: provider.send.bind(provider) }),
});
const text = result.ok
@@ -107,10 +112,12 @@ server.registerTool(
inputSchema: baseInputSchema,
},
async (input) => {
const config = loadConfig();
const provider = createChatProvider(config);
const result = await handleDebugIssue(input, {
loadConfig,
createOpenAIClient,
sendOpenAIResponse,
loadConfig: () => config,
createProvider: () => ({ send: provider.send.bind(provider) }),
});
const text = result.ok
@@ -135,10 +142,12 @@ server.registerTool(
inputSchema: baseInputSchema,
},
async (input) => {
const config = loadConfig();
const provider = createChatProvider(config);
const result = await handleArchitectureReview(input, {
loadConfig,
createOpenAIClient,
sendOpenAIResponse,
loadConfig: () => config,
createProvider: () => ({ send: provider.send.bind(provider) }),
});
const text = result.ok
+6 -12
View File
@@ -15,8 +15,7 @@ import { buildArchitectureReviewPrompt } from "../prompts/architecture-review.js
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
* createOpenAIClient: (config: object) => any,
* sendOpenAIResponse: (client: any, params: object) => Promise<any>
* createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -49,25 +48,20 @@ export async function handleArchitectureReview(input, deps) {
const promptMessages = buildArchitectureReviewPrompt(budget.input);
// --- 5. Create OpenAI client ---
// --- 5. Create chat provider ---
let client;
let provider;
try {
client = deps.createOpenAIClient(config);
provider = deps.createProvider(config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
// --- 6. Send to OpenAI ---
// --- 6. Send via provider ---
let aiResult;
try {
aiResult = await deps.sendOpenAIResponse(client, {
input: [{ role: "system", content: promptMessages }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
aiResult = await provider.send(budget.input, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
+7 -12
View File
@@ -15,8 +15,7 @@ import { buildAskChatGptPrompt } from "../prompts/ask-chatgpt.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
* createOpenAIClient: (config: object) => any,
* sendOpenAIResponse: (client: any, params: object) => Promise<any>
* createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -49,25 +48,21 @@ export async function handleAskChatGpt(input, deps) {
const promptMessages = buildAskChatGptPrompt(budget.input);
// --- 5. Create OpenAI client ---
// --- 5. Create chat provider ---
let client;
let provider;
try {
client = deps.createOpenAIClient(config);
provider = deps.createProvider(config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
// --- 6. Send to OpenAI ---
// --- 6. Send via provider ---
let aiResult;
try {
aiResult = await deps.sendOpenAIResponse(client, {
input: [{ role: "system", content: promptMessages }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
aiResult = await provider.send(budget.input, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
+6 -12
View File
@@ -15,8 +15,7 @@ import { buildDebugIssuePrompt } from "../prompts/debug-issue.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
* createOpenAIClient: (config: object) => any,
* sendOpenAIResponse: (client: any, params: object) => Promise<any>
* createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -49,25 +48,20 @@ export async function handleDebugIssue(input, deps) {
const promptMessages = buildDebugIssuePrompt(budget.input);
// --- 5. Create OpenAI client ---
// --- 5. Create chat provider ---
let client;
let provider;
try {
client = deps.createOpenAIClient(config);
provider = deps.createProvider(config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
// --- 6. Send to OpenAI ---
// --- 6. Send via provider ---
let aiResult;
try {
aiResult = await deps.sendOpenAIResponse(client, {
input: [{ role: "system", content: promptMessages }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
aiResult = await provider.send(budget.input, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
+6 -12
View File
@@ -15,8 +15,7 @@ import { buildReviewCodePrompt } from "../prompts/review-code.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
* createOpenAIClient: (config: object) => any,
* sendOpenAIResponse: (client: any, params: object) => Promise<any>
* createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -49,25 +48,20 @@ export async function handleReviewCode(input, deps) {
const promptMessages = buildReviewCodePrompt(budget.input);
// --- 5. Create OpenAI client ---
// --- 5. Create chat provider ---
let client;
let provider;
try {
client = deps.createOpenAIClient(config);
provider = deps.createProvider(config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
// --- 6. Send to OpenAI ---
// --- 6. Send via provider ---
let aiResult;
try {
aiResult = await deps.sendOpenAIResponse(client, {
input: [{ role: "system", content: promptMessages }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
aiResult = await provider.send(budget.input, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
+7 -12
View File
@@ -15,8 +15,7 @@ import { buildReviewPlanPrompt } from "../prompts/review-plan.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
* createOpenAIClient: (config: object) => any,
* sendOpenAIResponse: (client: any, params: object) => Promise<any>
* createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -49,25 +48,21 @@ export async function handleReviewPlan(input, deps) {
const promptMessages = buildReviewPlanPrompt(budget.input);
// --- 5. Create OpenAI client ---
// --- 5. Create chat provider ---
let client;
let provider;
try {
client = deps.createOpenAIClient(config);
provider = deps.createProvider(config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
// --- 6. Send to OpenAI ---
// --- 6. Send via provider ---
let aiResult;
try {
aiResult = await deps.sendOpenAIResponse(client, {
input: [{ role: "system", content: promptMessages }],
model: config.openaiModel,
temperature: config.temperature,
maxOutputTokens: config.maxOutputTokens,
});
aiResult = await provider.send(budget.input, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}