feat: add allowed Linear API operations to MCP config

This commit is contained in:
Do Siki
2025-09-05 03:10:17 +02:00
parent 7e7d3fb1cc
commit b6365543ef
36 changed files with 14648 additions and 1 deletions
+85
View File
@@ -0,0 +1,85 @@
import { GET, HEAD } from './route'
import { NextRequest } from 'next/server'
// Mock the process object for tests
const mockProcess = {
uptime: jest.fn().mockReturnValue(1234),
env: {
npm_package_version: '1.0.0',
NODE_ENV: 'test',
},
}
const originalProcess = global.process
beforeEach(() => {
global.process = { ...originalProcess, ...mockProcess } as any
})
afterEach(() => {
global.process = originalProcess
})
describe('/api/health', () => {
describe('GET request', () => {
it('should return successful health status with required data', async () => {
const response = await GET()
// Check response status
expect(response.status).toBe(200)
// Get JSON data
const data = await response.json()
// Verify required fields
expect(data).toHaveProperty('status', 'ok')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('uptime')
expect(data).toHaveProperty('version')
expect(data).toHaveProperty('environment')
// Verify timestamp is a valid ISO string
expect(() => new Date(data.timestamp)).not.toThrow()
expect(data.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
// Verify uptime matches mock
expect(data.uptime).toBe(1234)
// Verify version
expect(data.version).toBe('1.0.0')
})
it('should include correct Cache-Control headers', async () => {
const response = await GET()
expect(response.headers.get('Cache-Control')).toBe('no-cache, no-store, must-revalidate')
expect(response.headers.get('Pragma')).toBe('no-cache')
expect(response.headers.get('Expires')).toBe('0')
})
it('should handle errors gracefully', async () => {
// Temporarily break process.env to simulate error
global.process.env = undefined as any
const response = await GET()
expect(response.status).toBe(503)
const data = await response.json()
expect(data.status).toBe('error')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('message', 'Health check failed')
// Restore process.env
global.process.env = originalProcess.env
})
})
describe('HEAD request', () => {
it('should return 200 status without body', async () => {
const response = await HEAD()
expect(response.status).toBe(200)
expect(response.headers.get('Cache-Control')).toBe('no-cache, no-store, must-revalidate')
})
})
})
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
export async function GET() {
try {
// Basic health check
const healthData = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
};
return NextResponse.json(healthData, {
status: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
});
} catch (error) {
console.error('Health check error:', error);
return NextResponse.json(
{
status: 'error',
timestamp: new Date().toISOString(),
message: 'Health check failed',
},
{ status: 503 }
);
}
}
// Also support HEAD requests for lighter health checks
export async function HEAD() {
return new Response(null, {
status: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+76
View File
@@ -0,0 +1,76 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { siteConfig } from "../config/site";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: `${siteConfig.general.name} | ${siteConfig.general.description}`,
description: siteConfig.general.description,
authors: [{ name: siteConfig.general.name }],
keywords: ["web hosting", "email szolgáltatás", "DNS adminisztráció", "IT szolgáltatás", "mozdIT"],
openGraph: {
title: siteConfig.general.name,
description: siteConfig.general.description,
url: siteConfig.general.url,
siteName: siteConfig.general.name,
images: [
{
url: siteConfig.general.ogImage,
width: 1200,
height: 630,
alt: siteConfig.general.name,
},
],
locale: siteConfig.general.locale,
type: "website",
},
twitter: {
card: "summary_large_image",
title: siteConfig.general.name,
description: siteConfig.general.description,
images: [siteConfig.general.ogImage],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="hu">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
>
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</body>
</html>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { siteConfig } from '@/config/site'
export default function Home() {
return (
<div className="space-y-16">
{/* Hero Section */}
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
{siteConfig.hero.title}
</h1>
<p className="text-lg md:text-xl text-gray-600 max-w-4xl mx-auto mb-8 leading-relaxed">
{siteConfig.hero.description}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href={siteConfig.hero.cta.primary.href}
target={siteConfig.hero.cta.primary.external ? '_blank' : undefined}
rel={siteConfig.hero.cta.primary.external ? 'noopener noreferrer' : undefined}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-4 rounded-md transition-colors text-lg inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10" />
</svg>
{siteConfig.hero.cta.primary.text}
</a>
{siteConfig.hero.cta.secondary && (
<a
href={siteConfig.hero.cta.secondary.href}
className="border-2 border-blue-600 text-blue-600 hover:bg-blue-50 font-medium px-8 py-4 rounded-md transition-colors text-lg"
>
{siteConfig.hero.cta.secondary.text}
</a>
)}
</div>
</div>
</section>
{/* USP Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 bg-gray-50">
<div className="text-center">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-8">
{siteConfig.about.title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
{siteConfig.about.usps.map((usp) => (
<div key={usp.id} className="text-center">
<div className="w-16 h-16 bg-blue-100 group-hover:bg-blue-200 rounded-full flex items-center justify-center mx-auto mb-4 transition-colors">
<span className="text-2xl">{usp.icon}</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{usp.title}</h3>
<p className="text-gray-600">{usp.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Services Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
{siteConfig.services.title}
</h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
{siteConfig.services.subtitle}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{siteConfig.services.services.map((service) => (
<div key={service.id} className="group bg-white p-8 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-all duration-300 hover:border-blue-200">
<div className="w-12 h-12 bg-blue-100 group-hover:bg-blue-200 rounded-lg flex items-center justify-center mb-4 transition-colors">
<span className="text-xl">{service.icon}</span>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3 group-hover:text-blue-600 transition-colors">{service.title}</h3>
<p className="text-gray-600 leading-relaxed">
{service.description}
</p>
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Szolgáltatás jellemzők:</h4>
<ul className="text-sm text-gray-600 space-y-1">
{service.features.map((feature, index) => (
<li key={index} className="flex items-start">
<span className="text-blue-500 mr-2"></span>
{feature}
</li>
))}
</ul>
</div>
<a href="/kapcsolat" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium mt-4 transition-colors">
{service.ctaText}
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
))}
</div>
</section>
{/* CTA Section */}
<section className="bg-gray-900 text-white py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold mb-4">
Kapcsolatfelvétel az első lépés
</h2>
<p className="text-xl text-gray-300 mb-8">
Mutassuk meg, hogyan segíthetünk Önnek megvalósítani címeit!
</p>
<a
href="/kapcsolat"
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
>
Kapcsolatfelvétel
</a>
</div>
</section>
</div>
);
}