Publish @classroomio/mcp package

This commit is contained in:
Rotimi Best
2026-03-13 18:01:46 +01:00
parent 6e6f825e89
commit 77a1188573
9 changed files with 531 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
import type {
TCourseImportDraftCreate,
TCourseImportDraftPublish,
TCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
import type { McpServerConfig } from './config.js';
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiFailure = {
success?: false;
error?: string;
message?: string;
code?: string;
field?: string;
};
export class ClassroomIoApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string,
public field?: string
) {
super(message);
this.name = 'ClassroomIoApiError';
}
}
export class ClassroomIoApiClient {
constructor(private readonly config: McpServerConfig) {}
async createCourseDraft(payload: TCourseImportDraftCreate) {
return this.request('/organization/course-import/drafts', {
method: 'POST',
body: payload
});
}
async getCourseDraft(draftId: string) {
return this.request(`/organization/course-import/drafts/${draftId}`, {
method: 'GET'
});
}
async updateCourseDraft(draftId: string, payload: TCourseImportDraftUpdate) {
return this.request(`/organization/course-import/drafts/${draftId}`, {
method: 'PUT',
body: payload
});
}
async publishCourseDraft(draftId: string, payload: TCourseImportDraftPublish) {
return this.request(`/organization/course-import/drafts/${draftId}/publish`, {
method: 'POST',
body: payload
});
}
private async request<TResponse>(
path: string,
options: {
method: 'GET' | 'POST' | 'PUT';
body?: unknown;
}
): Promise<TResponse> {
const response = await fetch(new URL(path, this.config.CLASSROOMIO_API_URL), {
method: options.method,
headers: {
Authorization: `Bearer ${this.config.CLASSROOMIO_API_KEY}`,
'content-type': 'application/json',
'user-agent': this.config.CLASSROOMIO_USER_AGENT
},
body: options.body ? JSON.stringify(options.body) : undefined
});
const json = (await response.json().catch(() => null)) as ApiSuccess<TResponse> | ApiFailure | null;
if (!response.ok) {
const errorPayload = json as ApiFailure | null;
throw new ClassroomIoApiError(
errorPayload?.error ?? errorPayload?.message ?? `ClassroomIO request failed with status ${response.status}`,
response.status,
errorPayload?.code,
errorPayload?.field
);
}
if (!json || typeof json !== 'object' || !('success' in json) || !json.success) {
throw new ClassroomIoApiError('ClassroomIO returned an invalid response payload', response.status);
}
return json.data;
}
}
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod';
const ZConfig = z.object({
CLASSROOMIO_API_URL: z.string().url(),
CLASSROOMIO_API_KEY: z.string().min(1),
CLASSROOMIO_USER_AGENT: z.string().min(1).default('classroomio-mcp/0.0.1')
});
export type McpServerConfig = z.infer<typeof ZConfig>;
export function getConfig(env: NodeJS.ProcessEnv = process.env): McpServerConfig {
return ZConfig.parse(env);
}
+41
View File
@@ -0,0 +1,41 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ClassroomIoApiClient, ClassroomIoApiError } from './api-client.js';
import { getConfig } from './config.js';
import { registerCourseDraftTools } from './tools/course-drafts.js';
async function main() {
const config = getConfig();
const apiClient = new ClassroomIoApiClient(config);
const server = new McpServer({
name: 'classroomio-course-authoring',
version: '0.0.1'
});
registerCourseDraftTools(server, apiClient);
const transport = new StdioServerTransport();
await server.connect(transport);
process.stdin.on('close', () => {
void server.close();
});
}
main().catch((error: unknown) => {
if (error instanceof ClassroomIoApiError) {
console.error(
JSON.stringify({
error: error.message,
status: error.status,
code: error.code,
field: error.field
})
);
process.exit(1);
}
console.error(error);
process.exit(1);
});
+60
View File
@@ -0,0 +1,60 @@
import {
ZCourseImportDraftCreate,
ZCourseImportDraftGetParam,
ZCourseImportDraftPublish,
ZCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
import type { ClassroomIoApiClient } from '../api-client.js';
export const ZUpdateCourseDraftToolInput = ZCourseImportDraftUpdate.extend({
draftId: ZCourseImportDraftGetParam.shape.draftId
});
export const ZPublishCourseDraftToolInput = ZCourseImportDraftPublish.extend({
draftId: ZCourseImportDraftGetParam.shape.draftId
});
const createCourseDraftShape = ZCourseImportDraftCreate.shape as unknown as ZodRawShapeCompat;
const getCourseDraftShape = ZCourseImportDraftGetParam.shape as unknown as ZodRawShapeCompat;
const updateCourseDraftShape = ZUpdateCourseDraftToolInput.shape as unknown as ZodRawShapeCompat;
const publishCourseDraftShape = ZPublishCourseDraftToolInput.shape as unknown as ZodRawShapeCompat;
export function registerCourseDraftTools(server: McpServer, apiClient: ClassroomIoApiClient) {
server.tool('create_course_draft', createCourseDraftShape, async (args) => {
const payload = ZCourseImportDraftCreate.parse(args);
const result = await apiClient.createCourseDraft(payload);
return jsonContent(result);
});
server.tool('get_course_draft', getCourseDraftShape, async (args) => {
const { draftId } = ZCourseImportDraftGetParam.parse(args);
const result = await apiClient.getCourseDraft(draftId);
return jsonContent(result);
});
server.tool('update_course_draft', updateCourseDraftShape, async (args) => {
const { draftId, ...payload } = ZUpdateCourseDraftToolInput.parse(args);
const result = await apiClient.updateCourseDraft(draftId, payload);
return jsonContent(result);
});
server.tool('publish_course_draft', publishCourseDraftShape, async (args) => {
const { draftId, ...payload } = ZPublishCourseDraftToolInput.parse(args);
const result = await apiClient.publishCourseDraft(draftId, payload);
return jsonContent(result);
});
}
function jsonContent(data: unknown) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(data)
}
]
};
}