/**
* MT Data API クライアント
*/
import type {
MTConnection,
AuthToken,
MTSite,
MTEntry,
MTPage,
MTContentData,
MTContentType
} from '../types/index.js';
export class MTClient {
private connection: MTConnection;
private authToken: AuthToken | null = null;
private clientId = 'mt-content-refactor-mcp';
constructor(connection: MTConnection) {
this.connection = connection;
}
/**
* Data API のベースURL
*/
private get baseUrl(): string {
return `${this.connection.endpoint}/v6`;
}
/**
* 認証ヘッダーを取得
*/
private get authHeader(): Record<string, string> {
if (!this.authToken) {
return {};
}
return {
'X-MT-Authorization': `MTAuth accessToken=${this.authToken.accessToken}`,
};
}
/**
* 認証を実行
*/
async authenticate(): Promise<void> {
// 既存のトークンが有効な場合はスキップ
if (this.authToken && this.authToken.expiresAt > Date.now()) {
return;
}
const response = await fetch(`${this.baseUrl}/authentication`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
username: this.connection.username,
password: this.connection.password,
clientId: this.clientId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Authentication failed: ${error.error?.message || 'Unknown error'}`);
}
const data = await response.json();
this.authToken = {
accessToken: data.accessToken,
sessionId: data.sessionId,
expiresAt: Date.now() + (data.expiresIn * 1000) - 60000, // 1分前に期限切れとみなす
};
}
/**
* APIリクエストを実行
*/
private async request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
await this.authenticate();
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
...this.authHeader,
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
throw new Error(`API request failed: ${error.error?.message || response.statusText}`);
}
return response.json();
}
/**
* サイト一覧を取得
*/
async getSites(): Promise<MTSite[]> {
const data = await this.request<{ items: MTSite[]; totalResults: number }>('/sites');
return data.items;
}
/**
* 記事一覧を取得
*/
async getEntries(siteId: number, options: { limit?: number; offset?: number; status?: string } = {}): Promise<{ items: MTEntry[]; totalResults: number }> {
const params = new URLSearchParams();
if (options.limit) params.set('limit', options.limit.toString());
if (options.offset) params.set('offset', options.offset.toString());
if (options.status) params.set('status', options.status);
const query = params.toString();
const path = `/sites/${siteId}/entries${query ? `?${query}` : ''}`;
return this.request(path);
}
/**
* 記事を取得
*/
async getEntry(siteId: number, entryId: number): Promise<MTEntry> {
return this.request(`/sites/${siteId}/entries/${entryId}`);
}
/**
* 記事を更新
*/
async updateEntry(siteId: number, entryId: number, data: Partial<MTEntry>): Promise<MTEntry> {
return this.request(`/sites/${siteId}/entries/${entryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
entry: JSON.stringify(data),
}),
});
}
/**
* ウェブページ一覧を取得
*/
async getPages(siteId: number, options: { limit?: number; offset?: number; status?: string } = {}): Promise<{ items: MTPage[]; totalResults: number }> {
const params = new URLSearchParams();
if (options.limit) params.set('limit', options.limit.toString());
if (options.offset) params.set('offset', options.offset.toString());
if (options.status) params.set('status', options.status);
const query = params.toString();
const path = `/sites/${siteId}/pages${query ? `?${query}` : ''}`;
return this.request(path);
}
/**
* ウェブページを取得
*/
async getPage(siteId: number, pageId: number): Promise<MTPage> {
return this.request(`/sites/${siteId}/pages/${pageId}`);
}
/**
* ウェブページを更新
*/
async updatePage(siteId: number, pageId: number, data: Partial<MTPage>): Promise<MTPage> {
return this.request(`/sites/${siteId}/pages/${pageId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
page: JSON.stringify(data),
}),
});
}
/**
* コンテンツタイプ一覧を取得
*/
async getContentTypes(siteId: number): Promise<{ items: MTContentType[]; totalResults: number }> {
return this.request(`/sites/${siteId}/contentTypes`);
}
/**
* コンテンツデータ一覧を取得
*/
async getContentData(
siteId: number,
contentTypeId: number,
options: { limit?: number; offset?: number; status?: string } = {}
): Promise<{ items: MTContentData[]; totalResults: number }> {
const params = new URLSearchParams();
if (options.limit) params.set('limit', options.limit.toString());
if (options.offset) params.set('offset', options.offset.toString());
if (options.status) params.set('status', options.status);
const query = params.toString();
const path = `/sites/${siteId}/contentTypes/${contentTypeId}/data${query ? `?${query}` : ''}`;
return this.request(path);
}
/**
* コンテンツデータを取得
*/
async getContentDataItem(siteId: number, contentTypeId: number, contentDataId: number): Promise<MTContentData> {
return this.request(`/sites/${siteId}/contentTypes/${contentTypeId}/data/${contentDataId}`);
}
/**
* コンテンツデータを更新
*/
async updateContentData(
siteId: number,
contentTypeId: number,
contentDataId: number,
data: Partial<MTContentData>
): Promise<MTContentData> {
return this.request(`/sites/${siteId}/contentTypes/${contentTypeId}/data/${contentDataId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
content_data: JSON.stringify(data),
}),
});
}
/**
* 接続をテスト
*/
async testConnection(): Promise<{ success: boolean; message: string }> {
try {
await this.authenticate();
const sites = await this.getSites();
return {
success: true,
message: `接続成功。${sites.length} 件のサイトが見つかりました。`,
};
} catch (error) {
return {
success: false,
message: `接続失敗: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
}