import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import puppeteer from "puppeteer";
// Create server instance
const server = new McpServer({
name: "Sakarya University - MCP Server",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
} );
// Register SABIS login and grade scraping tool
server.tool(
"get-grades",
"Get grades of a student by logging into SABIS system",
{
username: {
type: "string",
description: "University username/student ID",
required: true,
},
password: {
type: "string",
description: "University password",
required: true,
},
},
async (args) => {
const username = process.env.USERNAME;
const password = process.env.PASSWORD;
if (!username || !password) {
throw new Error("Username and password are required. Please give them to your mcp host as environment variables.");
}
try {
// Launch browser
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Set user agent to avoid detection
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
// Navigate to login page
const loginUrl = 'https://login.sabis.sakarya.edu.tr/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3DSabis.Obs.WebUI%26redirect_uri%3Dhttps%253A%252F%252Fobs.sabis.sakarya.edu.tr%252Fsignin-oidc%26response_type%3Dcode%2520id_token%26scope%3Dopenid%2520profile%2520roles%26response_mode%3Dform_post%26nonce%3D638865586101464397.OGQ0YjdhMGMtMGZmMi00ODk3LWFmODgtOTM2OTYzZTAzODlmODNjNmU2NjMtMzE5NC00NDRkLTk5ZDgtN2ZjNDFiMDdkODEy%26state%3DCfDJ8Gcl3K2dWixFslXAXXJUu9siJv-d9xny73TJ_oNmfNCHjyLoCmjoAzfiDhoK7KLxlpp3IzAiNXG6D-Cx3HthDH1Y-tQ-_twGSAleUsm5kMYqLvnUyrl0vAFb08AdnsnshEKpu1pvWzNNhhRs1lMeBu1VcH4L32HkV3GrUeqyLsBkwGr4XAS2E9pOJo7PmN4u1862OOFA7do_fekek7QvIG4ucd8IrjJVStpELrnmqHAytn9rsgFfHtVl_f3BdJ9n09qNdJ8vzFGuB52J9McKyLRipk4o3egdmmuXSPpsIIYNprGQMAvvjYawKT14HGHGmw%26x-client-SKU%3DID_NET6_0%26x-client-ver%3D6.36.0.0';
console.log('Navigating to login page...');
await page.goto(loginUrl, { waitUntil: 'networkidle2' });
// Wait for login form to load
await page.waitForSelector('#Username', { timeout: 10000 });
// Fill username field
await page.type('#Username', username);
console.log('Username filled');
// Fill password field
await page.type('#Password', password);
console.log('Password filled');
// Submit the form by clicking the login button
await page.click('button[name="button"][value="login"]');
console.log('Login form submitted');
// Wait for navigation after login
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 });
// Check if login was successful by looking for specific elements or URLs
const currentUrl = page.url();
console.log('Current URL after login:', currentUrl);
if (currentUrl.includes('obs.sabis.sakarya.edu.tr')) {
console.log('Login successful, now accessing grades...');
// Try to navigate to grades page
try {
// Navigate directly to the grades page
console.log('Navigating to grades page...');
await page.goto('https://obs.sabis.sakarya.edu.tr/Ders');
await page.waitForSelector('.card', { timeout: 10000 });
// Extract detailed grades data
const gradesData = await page.evaluate(() => {
const courseCards = document.querySelectorAll('.card-custom');
const courses: string[] = [];
courseCards.forEach(card => {
// Extract course code from symbol
const courseCodeEl = card.querySelector('.symbol-label');
const courseCode = courseCodeEl?.textContent?.trim() || 'Unknown Code';
// Extract course name
const courseNameEl = card.querySelector('.text-dark.font-weight-bolder');
const courseName = courseNameEl?.textContent?.trim() || 'Unknown Course';
// Extract course group info
const courseGroupEl = card.querySelector('.text-muted.font-weight-bold');
const courseGroup = courseGroupEl?.textContent?.trim() || '';
let courseInfo = `\n=== ${courseCode} - ${courseName} ===`;
if (courseGroup) {
courseInfo += `\nGrup: ${courseGroup}`;
}
// Extract grades table
const table = card.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tbody tr');
let hasGrades = false;
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const percentage = cells[0]?.textContent?.trim() || '';
const assessmentType = cells[1]?.textContent?.trim() || '';
const grade = cells[2]?.textContent?.trim() || '';
if (percentage || assessmentType || grade) {
hasGrades = true;
if (assessmentType.includes('Başarı Notu')) {
courseInfo += `\n📋 Final Grade: ${grade}`;
} else {
courseInfo += `\n• ${assessmentType}${percentage ? ` (${percentage}%)` : ''}: ${grade || 'Not Available'}`;
}
}
}
});
if (!hasGrades) {
courseInfo += '\n• No grades available yet';
}
} else {
courseInfo += '\n• No grade information found';
}
courses.push(courseInfo);
});
return courses.length > 0 ? courses : ['No course data found'];
});
// Also get semester info
const semesterInfo = await page.evaluate(() => {
const yearSelect = document.querySelector('#yil') as HTMLSelectElement;
const semesterSelect = document.querySelector('#donem') as HTMLSelectElement;
const year = yearSelect?.value || 'Unknown';
const semester = semesterSelect?.selectedOptions[0]?.textContent || 'Unknown';
return `Academic Year: ${year} - Semester: ${semester}`;
});
await browser.close();
return {
content: [
{
type: "text",
text: `Login successful! 🎓\n\n${semesterInfo}\n${gradesData.join('\n')}`,
},
],
};
} catch (gradesError) {
console.error('Error accessing grades page:', gradesError);
await browser.close();
return {
content: [
{
type: "text",
text: `Login successful, but couldn't access grades page. Error: ${gradesError instanceof Error ? gradesError.message : String(gradesError)}`,
},
],
};
}
} else {
// Login failed
const errorMessage = await page.evaluate(() => {
const errorElements = document.querySelectorAll('.alert-danger, .error, .validation-summary-errors');
return Array.from(errorElements).map(el => el.textContent?.trim()).join('; ');
});
await browser.close();
return {
content: [
{
type: "text",
text: `Login failed. Error: ${errorMessage || 'Invalid credentials or unexpected redirect'}`,
},
],
};
}
} catch (error) {
console.error('Scraping error:', error);
return {
content: [
{
type: "text",
text: `Error during scraping: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
},
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});