feat(API-11): Route API calls through AgilitonAPI gateway

Add gateway-first pattern: when AGILITON_API_KEY is set, route all
external API calls through the gateway with X-API-Key auth. Falls back
to direct API access when gateway is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-11 19:05:19 +02:00
parent 6b53fb9168
commit 77097ac65f
2 changed files with 41 additions and 25 deletions

6
.env
View File

@@ -28,7 +28,11 @@ POSTGRES_PORT=6432
LLM_API_URL=https://api.agiliton.cloud/llm LLM_API_URL=https://api.agiliton.cloud/llm
LLM_API_KEY=sk-c02d41a118ce8330c428100afaa816c8 LLM_API_KEY=sk-c02d41a118ce8330c428100afaa816c8
# Jira Cloud (CF-762 migration) # AgilitonAPI Gateway (API-11: centralized API access)
AGILITON_API_KEY=gw_92399e154f02730ebadec65ddbde9426c9378ec77093d1c9
AGILITON_API_URL=https://api.agiliton.cloud
# Jira Cloud (fallback if gateway unavailable)
JIRA_URL=https://agiliton.atlassian.net JIRA_URL=https://agiliton.atlassian.net
JIRA_USERNAME=christian.gick@agiliton.eu JIRA_USERNAME=christian.gick@agiliton.eu
JIRA_API_TOKEN=ATATT3xFfGF0tpaJTS4nJklW587McubEw-1SYbLWqfovkxI5320NdbFc-3fgHlw0HGTLOikgV082m9N-SIsYVZveGXa553_1LAyOevV6Qples93xF4hIExWGAvwvXPy_4pW2tH5FNusN5ieMca5_-YUP0i69SIN0RLIMQjfqDmQyhZXbkIvrm-I=A8A2A1FC JIRA_API_TOKEN=ATATT3xFfGF0tpaJTS4nJklW587McubEw-1SYbLWqfovkxI5320NdbFc-3fgHlw0HGTLOikgV082m9N-SIsYVZveGXa553_1LAyOevV6Qples93xF4hIExWGAvwvXPy_4pW2tH5FNusN5ieMca5_-YUP0i69SIN0RLIMQjfqDmQyhZXbkIvrm-I=A8A2A1FC

View File

@@ -1,8 +1,9 @@
/** /**
* Jira Cloud REST API client for session-mcp. * Jira Cloud REST API client — routes through AgilitonAPI gateway.
* Creates/closes CF issues for sessions and posts session output as comments. * Falls back to direct Jira access if AGILITON_API_KEY is not set.
* *
* Uses JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN env vars. * Gateway: AGILITON_API_KEY + AGILITON_API_URL
* Direct: JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN
*/ */
interface JiraIssue { interface JiraIssue {
@@ -16,35 +17,48 @@ interface JiraTransition {
name: string; name: string;
} }
const getConfig = () => ({ // Gateway config
url: process.env.JIRA_URL || 'https://agiliton.atlassian.net', const GATEWAY_URL = (process.env.AGILITON_API_URL || 'https://api.agiliton.cloud').replace(/\/$/, '');
username: process.env.JIRA_USERNAME || '', const GATEWAY_KEY = process.env.AGILITON_API_KEY || '';
token: process.env.JIRA_API_TOKEN || '',
});
function getAuthHeader(): string { // Direct config (fallback)
const { username, token } = getConfig(); const JIRA_URL = process.env.JIRA_URL || 'https://agiliton.atlassian.net';
return `Basic ${Buffer.from(`${username}:${token}`).toString('base64')}`; const JIRA_USERNAME = process.env.JIRA_USERNAME || '';
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN || '';
function useGateway(): boolean {
return !!GATEWAY_KEY;
} }
function isConfigured(): boolean { function isConfigured(): boolean {
const { username, token } = getConfig(); if (useGateway()) return true;
return !!(username && token); return !!(JIRA_USERNAME && JIRA_API_TOKEN);
} }
async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> { async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> {
const { url } = getConfig(); let url: string;
const fullUrl = `${url}/rest/api/3${path}`; let headers: Record<string, string>;
return fetch(fullUrl, { if (useGateway()) {
...options, url = `${GATEWAY_URL}/jira-cloud${path}`;
headers: { headers = {
'Authorization': getAuthHeader(), 'X-API-Key': GATEWAY_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
...options.headers, ...options.headers as Record<string, string>,
}, };
}); } else {
url = `${JIRA_URL}/rest/api/3${path}`;
const auth = Buffer.from(`${JIRA_USERNAME}:${JIRA_API_TOKEN}`).toString('base64');
headers = {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers as Record<string, string>,
};
}
return fetch(url, { ...options, headers });
} }
/** /**
@@ -165,7 +179,6 @@ export async function transitionToDone(issueKey: string): Promise<boolean> {
if (!isConfigured()) return false; if (!isConfigured()) return false;
try { try {
// Get available transitions
const transResponse = await jiraFetch(`/issue/${issueKey}/transitions`); const transResponse = await jiraFetch(`/issue/${issueKey}/transitions`);
if (!transResponse.ok) { if (!transResponse.ok) {
console.error(`session-mcp: Jira get transitions failed (${transResponse.status})`); console.error(`session-mcp: Jira get transitions failed (${transResponse.status})`);
@@ -182,7 +195,6 @@ export async function transitionToDone(issueKey: string): Promise<boolean> {
return false; return false;
} }
// Execute transition
const response = await jiraFetch(`/issue/${issueKey}/transitions`, { const response = await jiraFetch(`/issue/${issueKey}/transitions`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({